From f68d5d956ae7ed5272f470f1e46e2ef32bf44cee Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:24:03 -0400 Subject: [PATCH 01/28] enabled Biome and format --- biome.jsonc | 63 + examples/next-smoke/app/globals.css | 27 +- examples/next-smoke/app/page.tsx | 3 +- examples/next-smoke/tsconfig.json | 10 +- package.json | 3 + packages/cli/src/adapters/git.ts | 38 +- packages/cli/src/adapters/local-state.ts | 65 +- packages/cli/src/adapters/mock-api.ts | 123 +- packages/cli/src/adapters/token-storage.ts | 23 +- packages/cli/src/bin.ts | 14 +- packages/cli/src/cli.ts | 39 +- packages/cli/src/commands/app/index.ts | 147 +- packages/cli/src/commands/auth/index.ts | 32 +- packages/cli/src/commands/branch/index.ts | 18 +- packages/cli/src/commands/database/index.ts | 120 +- packages/cli/src/commands/env.ts | 86 +- packages/cli/src/commands/git/index.ts | 47 +- packages/cli/src/commands/project/index.ts | 61 +- packages/cli/src/commands/version/index.ts | 8 +- packages/cli/src/controllers/app-env-api.ts | 32 +- packages/cli/src/controllers/app-env-file.ts | 68 +- packages/cli/src/controllers/app-env.ts | 321 +- packages/cli/src/controllers/app.ts | 1505 +++++--- packages/cli/src/controllers/auth.ts | 39 +- packages/cli/src/controllers/branch.ts | 65 +- packages/cli/src/controllers/database.ts | 197 +- packages/cli/src/controllers/project.ts | 589 +++- .../cli/src/controllers/select-prompt-port.ts | 4 +- packages/cli/src/controllers/version.ts | 4 +- .../cli/src/lib/app/branch-database-deploy.ts | 368 +- packages/cli/src/lib/app/branch-database.ts | 184 +- packages/cli/src/lib/app/bun-project.ts | 13 +- packages/cli/src/lib/app/deploy-output.ts | 25 +- packages/cli/src/lib/app/domain-guidance.ts | 4 +- packages/cli/src/lib/app/env-config.ts | 16 +- packages/cli/src/lib/app/env-file.ts | 21 +- packages/cli/src/lib/app/env-vars.ts | 34 +- packages/cli/src/lib/app/local-dev.ts | 63 +- .../src/lib/app/preview-branch-database.ts | 61 +- .../cli/src/lib/app/preview-build-settings.ts | 284 +- packages/cli/src/lib/app/preview-build.ts | 369 +- .../cli/src/lib/app/preview-interaction.ts | 19 +- packages/cli/src/lib/app/preview-progress.ts | 6 +- packages/cli/src/lib/app/preview-provider.ts | 292 +- .../cli/src/lib/app/production-deploy-gate.ts | 39 +- packages/cli/src/lib/auth/auth-ops.ts | 19 +- packages/cli/src/lib/auth/client.ts | 14 +- packages/cli/src/lib/auth/login.ts | 15 +- packages/cli/src/lib/database/provider.ts | 193 +- packages/cli/src/lib/git/local-branch.ts | 19 +- packages/cli/src/lib/git/local-status.ts | 17 +- .../cli/src/lib/project/interactive-setup.ts | 27 +- packages/cli/src/lib/project/local-pin.ts | 142 +- packages/cli/src/lib/project/resolution.ts | 361 +- packages/cli/src/lib/project/setup.ts | 79 +- packages/cli/src/lib/version.ts | 19 +- packages/cli/src/output/patterns.ts | 86 +- packages/cli/src/presenters/app-env.ts | 35 +- packages/cli/src/presenters/app.ts | 343 +- packages/cli/src/presenters/auth.ts | 4 +- packages/cli/src/presenters/branch.ts | 28 +- packages/cli/src/presenters/database.ts | 140 +- packages/cli/src/presenters/project.ts | 130 +- .../cli/src/presenters/verbose-context.ts | 35 +- packages/cli/src/presenters/version.ts | 6 +- packages/cli/src/shell/command-meta.ts | 134 +- packages/cli/src/shell/command-runner.ts | 45 +- packages/cli/src/shell/diagnostics-output.ts | 54 +- packages/cli/src/shell/errors.ts | 15 +- packages/cli/src/shell/global-flags.ts | 44 +- packages/cli/src/shell/help.ts | 80 +- packages/cli/src/shell/next-actions.ts | 12 +- packages/cli/src/shell/output.ts | 34 +- packages/cli/src/shell/runtime.ts | 14 +- packages/cli/src/shell/ui.ts | 61 +- packages/cli/src/shell/update-check.ts | 103 +- packages/cli/src/types/app.ts | 7 +- packages/cli/src/types/project.ts | 16 +- packages/cli/src/use-cases/auth.ts | 40 +- packages/cli/src/use-cases/branch.ts | 58 +- packages/cli/src/use-cases/contracts.ts | 41 +- .../cli/src/use-cases/create-cli-gateways.ts | 16 +- packages/cli/src/use-cases/project.ts | 33 +- .../cli/tests/app-branch-database.test.ts | 900 +++-- packages/cli/tests/app-build.test.ts | 726 ++-- packages/cli/tests/app-bun-compat.test.ts | 152 +- packages/cli/tests/app-controller.test.ts | 3075 +++++++++++------ packages/cli/tests/app-env-presenter.test.ts | 16 +- packages/cli/tests/app-env-vars.test.ts | 164 +- packages/cli/tests/app-env.test.ts | 348 +- packages/cli/tests/app-local-dev.test.ts | 96 +- packages/cli/tests/app-presenter.test.ts | 24 +- packages/cli/tests/app-provider.test.ts | 150 +- packages/cli/tests/app-state.test.ts | 35 +- packages/cli/tests/app.test.ts | 100 +- packages/cli/tests/auth-login.test.ts | 74 +- packages/cli/tests/auth-ops.test.ts | 167 +- packages/cli/tests/auth-real-mode.test.ts | 34 +- packages/cli/tests/auth.test.ts | 4 +- packages/cli/tests/branch-controller.test.ts | 110 +- packages/cli/tests/branch.test.ts | 21 +- .../cli/tests/command-runner-auth.test.ts | 24 +- packages/cli/tests/command-runner.test.ts | 47 +- packages/cli/tests/database.test.ts | 148 +- packages/cli/tests/git-adapter.test.ts | 33 +- packages/cli/tests/helpers.ts | 32 +- packages/cli/tests/helpers/mock-factories.ts | 114 +- packages/cli/tests/output.test.ts | 35 +- .../cli/tests/production-deploy-gate.test.ts | 98 +- packages/cli/tests/project-controller.test.ts | 56 +- packages/cli/tests/project-real-mode.test.ts | 769 +++-- packages/cli/tests/project-resolution.test.ts | 56 +- packages/cli/tests/project.test.ts | 241 +- packages/cli/tests/publish-prep.test.ts | 72 +- .../cli/tests/resolve-cli-version.test.ts | 25 +- packages/cli/tests/shell.test.ts | 40 +- packages/cli/tests/token-storage.test.ts | 14 +- packages/cli/tests/update-check.test.ts | 145 +- packages/cli/tests/use-case-helpers.ts | 15 +- packages/cli/tests/version.test.ts | 30 +- packages/compute/src/scale-to-zero-control.ts | 4 +- packages/compute/tests/scale-to-zero.test.ts | 4 +- pnpm-lock.yaml | 95 + scripts/prepare-cli-publish.mjs | 38 +- scripts/resolve-cli-version.mjs | 29 +- scripts/smoke-cli-nextjs-artifact.mjs | 13 +- scripts/validate-skills.mjs | 17 +- scripts/validate-skills.test.mjs | 30 +- 128 files changed, 11633 insertions(+), 4855 deletions(-) create mode 100644 biome.jsonc diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 0000000..df7bfd7 --- /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..b294c93 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.

  1. 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..8036952 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "scripts": { "build:cli": "pnpm --filter @prisma/cli build", "build:compute": "pnpm --filter @prisma/compute build", + "format": "biome format . --write", + "lint": "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 +17,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..27a9712 100644 --- a/packages/cli/src/adapters/git.ts +++ b/packages/cli/src/adapters/git.ts @@ -11,13 +11,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 +37,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 +60,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 +79,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..43a69d4 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,14 +303,18 @@ 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; } } diff --git a/packages/cli/src/adapters/token-storage.ts b/packages/cli/src/adapters/token-storage.ts index 5d9e9c4..fb5d963 100644 --- a/packages/cli/src/adapters/token-storage.ts +++ b/packages/cli/src/adapters/token-storage.ts @@ -11,7 +11,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 +65,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`; @@ -161,10 +166,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 +183,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..ab758ce 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -20,10 +20,12 @@ 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(() => { - process.off("SIGINT", abortCli); - process.off("SIGTERM", abortCli); - }); + runCli({ signal: controller.signal }) + .then((exitCode) => { + process.exitCode = exitCode; + }) + .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..a05929c 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 { + configureRuntimeCommand, + createCommandContext, + type CliRuntime, +} 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..078fe6c 100644 --- a/packages/cli/src/commands/app/index.ts +++ b/packages/cli/src/commands/app/index.ts @@ -50,7 +50,10 @@ import { } from "../../presenters/app"; import { attachCommandDescriptor } from "../../shell/command-meta"; import { usageError } from "../../shell/errors"; -import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags"; +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"; @@ -72,7 +75,10 @@ import type { } 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..887a4c8 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 { + runAuthLogin, + runAuthLogout, + runAuthWhoAmI, + type AuthLoginCommandOptions, +} from "../../controllers/auth"; import { renderAuthSuccess } from "../../presenters/auth"; import { attachCommandDescriptor } from "../../shell/command-meta"; -import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags"; +import { + addCompactGlobalFlags, + addGlobalFlags, +} from "../../shell/global-flags"; import { runCommand } from "../../shell/command-runner"; import { configureRuntimeCommand, type CliRuntime } 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..24b9fb5 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 { + addCompactGlobalFlags, + addGlobalFlags, +} from "../../shell/global-flags"; import { runCommand } from "../../shell/command-runner"; import { configureRuntimeCommand, type CliRuntime } 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..4d5cb6e 100644 --- a/packages/cli/src/commands/database/index.ts +++ b/packages/cli/src/commands/database/index.ts @@ -29,7 +29,10 @@ import { } from "../../presenters/database"; import { attachCommandDescriptor } from "../../shell/command-meta"; import { runCommand } from "../../shell/command-runner"; -import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags"; +import { + addCompactGlobalFlags, + addGlobalFlags, +} from "../../shell/global-flags"; import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime"; import type { DatabaseConnectionCreateResult, @@ -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..c14dbe2 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, @@ -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..1c8fc4b 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 { + renderGitConnect, + renderGitDisconnect, +} from "../../presenters/project"; import { runCommand } from "../../shell/command-runner"; import { attachCommandDescriptor } from "../../shell/command-meta"; -import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags"; +import { + addCompactGlobalFlags, + addGlobalFlags, +} from "../../shell/global-flags"; import { configureRuntimeCommand, type CliRuntime } 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..eb84df4 100644 --- a/packages/cli/src/commands/project/index.ts +++ b/packages/cli/src/commands/project/index.ts @@ -1,6 +1,11 @@ import { Command } from "commander"; -import { runProjectCreate, runProjectLink, runProjectList, runProjectShow } from "../../controllers/project"; +import { + runProjectCreate, + runProjectLink, + runProjectList, + runProjectShow, +} from "../../controllers/project"; import { renderProjectSetup, renderProjectList, @@ -10,14 +15,24 @@ import { serializeProjectShow, } from "../../presenters/project"; import { attachCommandDescriptor } from "../../shell/command-meta"; -import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags"; +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 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..fb5d8a7 100644 --- a/packages/cli/src/commands/version/index.ts +++ b/packages/cli/src/commands/version/index.ts @@ -9,7 +9,10 @@ import { configureRuntimeCommand, type CliRuntime } 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..b371ace 100644 --- a/packages/cli/src/controllers/app-env-file.ts +++ b/packages/cli/src/controllers/app-env-file.ts @@ -88,9 +88,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 +173,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 +221,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 +255,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 +292,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..82a05c9 100644 --- a/packages/cli/src/controllers/app-env.ts +++ b/packages/cli/src/controllers/app-env.ts @@ -7,13 +7,24 @@ import { type EnvScope, type EnvVarRole, } from "../lib/app/env-config"; -import { readEnvFileAssignments, type EnvFileAssignment } from "../lib/app/env-file"; +import { + readEnvFileAssignments, + type EnvFileAssignment, +} 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 { + 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 { + projectResolutionErrorToCliError, + resolveProjectTarget, +} from "../lib/project/resolution"; import type { EnvAddResult, EnvListTarget, @@ -81,7 +92,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 +107,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 +153,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 +175,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 +193,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 +209,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 +224,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 +277,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 +290,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 +356,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 +378,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 +421,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 +445,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 +459,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 +483,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,9 +515,16 @@ 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"]); } @@ -420,7 +536,12 @@ async function requireClientAndProject( context, workspace: authState.workspace, explicitProject, - listProjects: () => listRealWorkspaceProjects(client, authState.workspace!, context.runtime.signal), + listProjects: () => + listRealWorkspaceProjects( + client, + authState.workspace!, + context.runtime.signal, + ), commandName, }); if (targetResult.isErr()) { @@ -454,8 +575,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 +633,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 +748,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 +764,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 +775,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 +789,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 +818,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 +850,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 +884,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 +902,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 +953,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 +1006,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..30e717c 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -7,7 +7,13 @@ import type { ManagementApiClient } from "@prisma/management-api-sdk"; import { Result, matchError } from "better-result"; import { FileTokenStorage } from "../adapters/token-storage"; -import { authRequiredError, CliError, featureUnavailableError, usageError, workspaceRequiredError } from "../shell/errors"; +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"; @@ -41,13 +47,20 @@ import { requireComputeAuth } from "../lib/auth/guard"; import { readAuthState } from "../lib/auth/auth-ops"; import { getApiBaseUrl, SERVICE_TOKEN_ENV_VAR } from "../lib/auth/client"; import { envVarNames, parseEnvInputs } from "../lib/app/env-vars"; -import { renderDeployOutputRows, renderDeploySettingsPreview } from "../lib/app/deploy-output"; +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 { + readBunPackageEntrypoint, + readBunPackageJson, + type BunPackageJsonLike, +} from "../lib/app/bun-project"; import { buildProjectSetupNextActions, inferTargetName, @@ -87,7 +100,10 @@ import { type PreviewBuildType, } 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 { + maybeSetupBranchDatabase, + type BranchDatabaseDeployBranch, +} from "../lib/app/branch-database-deploy"; import { createPreviewDeployProgress, createPreviewDeployProgressState, @@ -109,7 +125,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 +140,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 +206,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 +266,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 +277,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 +296,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 +316,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 +336,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 +367,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 +395,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 +487,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 +501,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 +527,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 +568,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 +582,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 +610,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 +658,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 +673,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 +693,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 +738,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 +757,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 +829,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 +846,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 +887,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 +926,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 +969,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 +1010,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 +1053,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 +1066,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 +1089,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 +1109,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 +1150,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 +1174,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 +1188,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 +1212,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 +1277,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 +1291,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 +1359,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 +1397,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 +1435,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 +1453,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 +1479,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 +1501,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 +1540,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 +1557,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 +1633,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 +1676,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 +1708,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 +1734,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 +1770,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 +1784,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 +1814,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 +1848,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`, + ], }); } @@ -1502,7 +1871,9 @@ async function confirmDomainRemoval( "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`], + [ + `prisma-cli app domain remove ${hostname} --app ${target.app.name} --yes`, + ], "app", ); } @@ -1514,7 +1885,11 @@ function domainCommandError( hostname: string, ): CliError { if (error instanceof PreviewDomainApiError) { - if (command === "add" && (error.status === 400 || error.status === 422) && isDomainDnsError(error)) { + if ( + command === "add" && + (error.status === 400 || error.status === 422) && + isDomainDnsError(error) + ) { return domainDnsNotConfiguredError(hostname, error); } @@ -1531,7 +1906,10 @@ function domainCommandError( }); } - if (command === "add" && (error.status === 429 || isDomainQuotaError(error))) { + if ( + command === "add" && + (error.status === 429 || isDomainQuotaError(error)) + ) { return new CliError({ code: "DOMAIN_QUOTA_EXCEEDED", domain: "app", @@ -1564,7 +1942,13 @@ function domainCommandError( }); } - if ((command === "show" || command === "remove" || command === "retry" || command === "wait") && error.status === 404) { + if ( + (command === "show" || + command === "remove" || + command === "retry" || + command === "wait") && + error.status === 404 + ) { return domainNotFoundError(hostname); } @@ -1600,10 +1984,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 +2020,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 +2038,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 +2094,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 +2152,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 +2203,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 +2252,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 +2274,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 +2298,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 +2385,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 +2436,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 +2480,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 +2512,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 +2536,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 +2605,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 +2629,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 +2661,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 +2700,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 +2735,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 +2766,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, @@ -2336,14 +2803,20 @@ async function resolveProjectContext( workspace: authState.workspace, explicitProject, envProjectId: options?.envProjectId, - listProjects: () => listRealWorkspaceProjects(client, authState.workspace!, context.runtime.signal), + listProjects: () => + listRealWorkspaceProjects( + client, + authState.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,21 +2846,35 @@ 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, + ); if (explicitProject) { - const project = resolveProjectForSetup(explicitProject, projects, workspace); - return withRemoteDeployBranch(provider, { + const project = resolveProjectForSetup( + explicitProject, + projects, workspace, - project: toProjectSummary(project), - resolution: { - projectSource: "explicit", - targetName: explicitProject, - targetNameSource: "explicit", + ); + return withRemoteDeployBranch( + provider, + { + workspace, + project: toProjectSummary(project), + resolution: { + projectSource: "explicit", + targetName: explicitProject, + targetNameSource: "explicit", + }, + localPinAction: "linked", }, - localPinAction: "linked", - }, branch, context.runtime.signal); + branch, + context.runtime.signal, + ); } if (options.createProjectName) { @@ -2396,33 +2883,50 @@ 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, - project: toProjectSummary(created), - resolution: { - projectSource: "created", - targetName: projectName, - targetNameSource: "explicit", + context.runtime.signal, + ); + return withRemoteDeployBranch( + provider, + { + workspace, + project: toProjectSummary(created), + resolution: { + projectSource: "created", + targetName: projectName, + targetNameSource: "explicit", + }, + localPinAction: "created", }, - localPinAction: "created", - }, branch, context.runtime.signal); + 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, { - workspace, - project: toProjectSummary(project), - resolution: { - projectSource: "env", - targetName: options.envProjectId, - targetNameSource: "env", + return withRemoteDeployBranch( + provider, + { + workspace, + project: toProjectSummary(project), + resolution: { + projectSource: "env", + targetName: options.envProjectId, + targetNameSource: "env", + }, }, - }, branch, context.runtime.signal); + branch, + context.runtime.signal, + ); } const localPin = options.localPin; @@ -2431,41 +2935,66 @@ 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, { - workspace, - project: toProjectSummary(project), - resolution: { - projectSource: "local-pin", - targetName: project.name, - targetNameSource: "local-pin", + return withRemoteDeployBranch( + provider, + { + workspace, + project: toProjectSummary(project), + resolution: { + projectSource: "local-pin", + targetName: project.name, + targetNameSource: "local-pin", + }, }, - }, branch, context.runtime.signal); + branch, + context.runtime.signal, + ); } const platformMapping = await resolveDurablePlatformMapping(); if (platformMapping && platformMapping.workspace.id === workspace.id) { - return withRemoteDeployBranch(provider, { - workspace, - project: toProjectSummary(platformMapping), - resolution: { - projectSource: "platform-mapping", - targetName: platformMapping.name, - targetNameSource: "platform-mapping", + return withRemoteDeployBranch( + provider, + { + workspace, + project: toProjectSummary(platformMapping), + resolution: { + projectSource: "platform-mapping", + targetName: platformMapping.name, + targetNameSource: "platform-mapping", + }, }, - }, branch, context.runtime.signal); + 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 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); + const suggestedName = await inferTargetName( + context.runtime.cwd, + context.runtime.signal, + ); throw projectSetupRequiredError(projects, suggestedName); } @@ -2478,11 +3007,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 +3042,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 +3090,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 +3100,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 +3111,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 +3160,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 +3171,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 +3208,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 +3223,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 +3259,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 +3302,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 +3343,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 +3377,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 +3447,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 +3487,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 +3504,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 +3537,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 +3552,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 +3607,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 +3662,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 +3683,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 +3703,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 +3791,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 +3831,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 +3870,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,17 +3887,22 @@ 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." + ? '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"] + ? [ + 'Add output: "standalone" to next.config.*, then rerun prisma-cli app deploy', + ] : []; const nextActions = standaloneOutputFailure ? [ @@ -3274,7 +3910,8 @@ function appDeployFailedError(error: unknown, progress: PreviewDeployProgressSta 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.", + reason: + "Prisma Compute needs Next.js standalone output to build a deployable server artifact.", }, { kind: "run-command" as const, @@ -3307,15 +3944,23 @@ function appDeployFailedError(error: unknown, progress: PreviewDeployProgressSta } 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] : []; @@ -3373,11 +4018,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 +4042,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 +4083,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 +4118,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 +4139,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 +4162,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 +4196,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..a3ed586 100644 --- a/packages/cli/src/controllers/auth.ts +++ b/packages/cli/src/controllers/auth.ts @@ -6,7 +6,11 @@ import { createAuthUseCases } from "../use-cases/auth"; import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways"; import type { LoginSelection, SelectPromptPort } from "../use-cases/contracts"; import { createSelectPromptPort } from "./select-prompt-port"; -import { performLogin, readAuthState, performLogout } from "../lib/auth/auth-ops"; +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..09c2f50 100644 --- a/packages/cli/src/controllers/branch.ts +++ b/packages/cli/src/controllers/branch.ts @@ -1,18 +1,32 @@ import type { ManagementApiClient } from "@prisma/management-api-sdk"; -import { authRequiredError, CliError, workspaceRequiredError } from "../shell/errors"; +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 type { + BranchListResult, + BranchRole, + BranchSummary, +} from "../types/branch"; import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways"; import { createBranchUseCases } from "../use-cases/branch"; import { requireComputeAuth } from "../lib/auth/guard"; -import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution"; +import { + projectResolutionErrorToCliError, + resolveProjectTarget, +} from "../lib/project/resolution"; 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..2dcc62f 100644 --- a/packages/cli/src/controllers/database.ts +++ b/packages/cli/src/controllers/database.ts @@ -7,8 +7,17 @@ import { 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, + resolveProjectTarget, + type ResolvedProjectTarget, +} 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..2d39756 100644 --- a/packages/cli/src/controllers/project.ts +++ b/packages/cli/src/controllers/project.ts @@ -19,7 +19,10 @@ import { type ResolvedProjectTarget, } from "../lib/project/resolution"; import { promptForProjectSetupChoice } from "../lib/project/interactive-setup"; -import { readLocalResolutionPin, type LocalResolutionPinReadError } from "../lib/project/local-pin"; +import { + readLocalResolutionPin, + type LocalResolutionPinReadError, +} from "../lib/project/local-pin"; import { bindProjectToDirectory, formatCommandArgument, @@ -31,7 +34,13 @@ import { 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 { renderSummaryLine } from "../shell/ui"; @@ -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, @@ -761,13 +948,15 @@ interface SourceRepositoryApiClient { }; signal?: AbortSignal; }, - ): Promise>; + ): Promise< + SourceRepositoryApiResult<{ + data: SourceRepositoryResponse[]; + pagination: { + nextCursor: string | null; + hasMore: boolean; + }; + }> + >; GET( path: "/v1/scm-installations", options: { @@ -780,13 +969,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,13 +992,15 @@ interface SourceRepositoryApiClient { }; signal?: AbortSignal; }, - ): Promise>; + ): Promise< + SourceRepositoryApiResult<{ + data: ScmRepositoryResponse[]; + pagination: { + nextCursor: string | null; + hasMore: boolean; + }; + }> + >; POST( path: "/v1/scm-installations/install-intents", options: { @@ -817,14 +1010,16 @@ interface SourceRepositoryApiClient { }; signal?: AbortSignal; }, - ): Promise>; + ): Promise< + SourceRepositoryApiResult<{ + data: { + type: "install-intent"; + provider: "github"; + workspaceId: string; + installUrl: string; + }; + }> + >; 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,12 @@ async function findRepositoryInInstallations( continue; } - const matchedRepository = await findRepositoryInInstallationIfAvailable(api, installation.id, repository, signal); + const matchedRepository = await findRepositoryInInstallationIfAvailable( + api, + installation.id, + repository, + signal, + ); if (matchedRepository === "unavailable") { continue; } @@ -941,7 +1161,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 +1178,18 @@ async function waitForInstalledRepository( while (Date.now() <= deadline) { context.runtime.signal.throwIfAborted(); - const installations = await listScmInstallations(api, workspaceId, context.runtime.signal); + 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 +1206,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; } @@ -1046,7 +1281,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 +1311,33 @@ 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 } : {}), + 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 +1359,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 +1386,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 +1415,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 +1473,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 +1508,9 @@ function createPendingRepositoryConnection( }; } -function toRepositoryConnection(record: SourceRepositoryResponse): GitRepositoryConnection { +function toRepositoryConnection( + record: SourceRepositoryResponse, +): GitRepositoryConnection { const [owner = "", name = ""] = record.repoFullName.split("/"); return { @@ -1314,10 +1583,7 @@ function repoInstallationRequiredError( opened, }, exitCode: 1, - nextSteps: [ - installUrl, - `prisma-cli git connect ${repository.url}`, - ], + nextSteps: [installUrl, `prisma-cli git connect ${repository.url}`], }); } @@ -1338,10 +1604,7 @@ function repoNotAccessibleError( opened, }, exitCode: 1, - nextSteps: [ - installUrl, - `prisma-cli git connect ${repository.url}`, - ], + nextSteps: [installUrl, `prisma-cli git connect ${repository.url}`], }); } @@ -1382,7 +1645,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..e9a1f84 100644 --- a/packages/cli/src/lib/app/branch-database-deploy.ts +++ b/packages/cli/src/lib/app/branch-database-deploy.ts @@ -79,38 +79,56 @@ export async function maybeSetupBranchDatabase( return emptyBranchDatabaseSetupOutcome(); } - const envState = await inspectBranchDatabaseEnv(provider, projectId, branch, context.runtime.signal); + const envState = await inspectBranchDatabaseEnv( + provider, + projectId, + branch, + context.runtime.signal, + ); const targetEnvVars = getTargetDatabaseEnvVarKeys(envState); if (hasExistingDatabaseEnvForTarget(branch, envState)) { - const warning = options.db === true ? existingDatabaseEnvWarning(branch, targetEnvVars) : null; + const warning = + options.db === true + ? existingDatabaseEnvWarning(branch, targetEnvVars) + : null; if (warning) { emitBranchDatabaseWarning(context, warning); } return { - result: options.db === true - ? { - status: "skipped", - reason: existingDatabaseEnvReason(branch), - envVars: targetEnvVars, - schema: null, - } - : undefined, + result: + options.db === true + ? { + status: "skipped", + reason: existingDatabaseEnvReason(branch), + envVars: targetEnvVars, + schema: null, + } + : undefined, warnings: warning ? [warning] : [], }; } - const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal); + const localSignal = await inspectBranchDatabaseSignal( + context.runtime.cwd, + context.runtime.signal, + ); if (localSignal.unsupportedSchema) { if (options.db === true) { - throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch, context); + throw unsupportedBranchDatabaseSchemaError( + localSignal.unsupportedSchema, + branch, + context, + ); } return emptyBranchDatabaseSetupOutcome(); } - const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.inheritedPreviewDatabaseUrl); + const hasSignal = + hasBranchDatabaseSignal(localSignal) || + Boolean(envState.inheritedPreviewDatabaseUrl); if (options.db !== true) { if (!hasSignal) { return emptyBranchDatabaseSetupOutcome(); @@ -140,7 +158,14 @@ export async function maybeSetupBranchDatabase( throw nonInteractiveDatabaseSetupRequiresYesError(branch); } - return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState); + return setupBranchDatabase( + context, + provider, + projectId, + branch, + localSignal, + envState, + ); } async function setupBranchDatabase( @@ -152,14 +177,20 @@ 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 { @@ -167,22 +198,43 @@ async function setupBranchDatabase( const warnings: string[] = []; let skippedSchemaWarning: string | null = null; if (signal.schema) { - emitBranchDatabaseProgress(context, "pending", `Applying database schema with ${formatSchemaSetupCommand(signal.schema.command)}`); + emitBranchDatabaseProgress( + context, + "pending", + `Applying database schema with ${formatSchemaSetupCommand(signal.schema.command)}`, + ); schemaSetup = await runBranchDatabaseSchemaSetup({ context, schema: signal.schema, databaseUrl: database.databaseUrl, directUrl: database.directUrl, }).catch((error) => { - throw schemaSetupFailedError(error, signal.schema!, branch, context.runtime.cwd); + throw schemaSetupFailedError( + error, + signal.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 +259,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 +300,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 +331,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 +389,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 +407,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 +428,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 +450,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 +469,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 +508,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 +528,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 +568,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 +585,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 +598,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 +628,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 +666,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 +721,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 +736,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 +782,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 +820,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..8caf231 100644 --- a/packages/cli/src/lib/app/branch-database.ts +++ b/packages/cli/src/lib/app/branch-database.ts @@ -5,7 +5,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 +91,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 +150,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({ @@ -225,11 +249,13 @@ async function scanDirectory( } if ( - state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES - && shouldScanForDatabaseUrl(entry.name) - && await fileContainsDatabaseUrl(entryPath, signal) + state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && + shouldScanForDatabaseUrl(entry.name) && + (await fileContainsDatabaseUrl(entryPath, signal)) ) { - state.databaseUrlReferences.push(path.relative(cwd, entryPath) || entry.name); + state.databaseUrlReferences.push( + path.relative(cwd, entryPath) || entry.name, + ); } } } @@ -244,7 +270,10 @@ async function selectPrismaOrmSchema( for (const schemaPath of sorted) { const target = await classifyPrismaOrmSchemaTarget(schemaPath, signal); if (target === "postgresql" || target === "unknown") { - const hasMigrations = await hasMigrationsDirectory(path.dirname(schemaPath), signal); + const hasMigrations = await hasMigrationsDirectory( + path.dirname(schemaPath), + signal, + ); return { schema: { kind: "prisma-orm", @@ -279,16 +308,31 @@ function selectPrismaNextConfig( mode: "supported" | "unsupported", ): ClassifiedPrismaNextConfig | null { const matches = candidates.filter((candidate) => { - const isSupported = candidate.target === "postgresql" || candidate.target === "unknown"; + const isSupported = + candidate.target === "postgresql" || candidate.target === "unknown"; return mode === "supported" ? isSupported : !isSupported; }); - return sortByPreferredRelativePath(cwd, matches.map((candidate) => candidate.path), "prisma-next.config.ts") - .map((candidatePath) => matches.find((candidate) => candidate.path === candidatePath)) - .find((candidate): candidate is ClassifiedPrismaNextConfig => Boolean(candidate)) ?? null; + return ( + sortByPreferredRelativePath( + cwd, + matches.map((candidate) => candidate.path), + "prisma-next.config.ts", + ) + .map((candidatePath) => + matches.find((candidate) => candidate.path === candidatePath), + ) + .find((candidate): candidate is ClassifiedPrismaNextConfig => + Boolean(candidate), + ) ?? null + ); } -function sortByPreferredRelativePath(cwd: string, candidates: string[], preferredRootFile: string): string[] { +function sortByPreferredRelativePath( + cwd: string, + candidates: string[], + preferredRootFile: string, +): string[] { return candidates .map((candidate) => ({ absolute: candidate, @@ -297,13 +341,18 @@ function sortByPreferredRelativePath(cwd: string, candidates: string[], preferre .sort((left, right) => { if (left.relative === preferredRootFile) return -1; if (right.relative === preferredRootFile) return 1; - return left.relative.length - right.relative.length - || left.relative.localeCompare(right.relative); + return ( + left.relative.length - right.relative.length || + left.relative.localeCompare(right.relative) + ); }) .map((candidate) => candidate.absolute); } -async function hasMigrationsDirectory(schemaDirectory: string, signal: AbortSignal): Promise { +async function hasMigrationsDirectory( + schemaDirectory: string, + signal: AbortSignal, +): Promise { signal.throwIfAborted(); const migrationsPath = path.join(schemaDirectory, "migrations"); @@ -393,15 +442,23 @@ function isPrismaNextConfigFile(fileName: string): boolean { return false; } - return [".cjs", ".cts", ".js", ".mjs", ".mts", ".ts"].some((extension) => fileName.endsWith(extension)); + return [".cjs", ".cts", ".js", ".mjs", ".mts", ".ts"].some((extension) => + fileName.endsWith(extension), + ); } -async function fileContainsDatabaseUrl(filePath: string, signal: AbortSignal): Promise { +async function fileContainsDatabaseUrl( + filePath: string, + signal: AbortSignal, +): Promise { const content = await readTextFileIfSmall(filePath, signal); return content?.includes("DATABASE_URL") ?? false; } -async function readTextFileIfSmall(filePath: string, signal: AbortSignal): Promise { +async function readTextFileIfSmall( + filePath: string, + signal: AbortSignal, +): Promise { signal.throwIfAborted(); const info = await stat(filePath); @@ -466,7 +523,9 @@ async function fileExists(filePath: string): Promise { } } -async function readInstalledPrismaClientVersion(cwd: string): Promise { +async function readInstalledPrismaClientVersion( + cwd: string, +): Promise { try { const raw = await readFile( path.join(cwd, "node_modules", "@prisma", "client", "package.json"), @@ -483,38 +542,71 @@ async function readInstalledPrismaClientVersion(cwd: string): Promise { if (schema.command === "migrate-deploy") { - return [{ - args: [...prisma.argsPrefix, "migrate", "deploy", "--schema", schemaPath], - displayCommand: `${prisma.displayPrefix} migrate deploy`, - }]; + return [ + { + args: [ + ...prisma.argsPrefix, + "migrate", + "deploy", + "--schema", + schemaPath, + ], + displayCommand: `${prisma.displayPrefix} migrate deploy`, + }, + ]; } if (schema.command === "db-push") { - return [{ - args: [...prisma.argsPrefix, "db", "push", "--schema", schemaPath], - displayCommand: `${prisma.displayPrefix} db push`, - }]; + return [ + { + args: [...prisma.argsPrefix, "db", "push", "--schema", schemaPath], + displayCommand: `${prisma.displayPrefix} db push`, + }, + ]; } return [ { - args: ["--no-install", "prisma-next", "contract", "emit", "--config", schemaPath], + args: [ + "--no-install", + "prisma-next", + "contract", + "emit", + "--config", + schemaPath, + ], displayCommand: "npx --no-install prisma-next contract emit", }, { - args: ["--no-install", "prisma-next", "db", "init", "--config", schemaPath, "--db", databaseUrl], + args: [ + "--no-install", + "prisma-next", + "db", + "init", + "--config", + schemaPath, + "--db", + databaseUrl, + ], displayCommand: "npx --no-install prisma-next db init", }, ]; } 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"; } async function runPrismaCommand(options: { @@ -523,7 +615,8 @@ async function runPrismaCommand(options: { displayCommand: string; env: Record; }): Promise { - const shouldPipeOutput = !options.context.flags.json && !options.context.flags.quiet; + const shouldPipeOutput = + !options.context.flags.json && !options.context.flags.quiet; const child = spawn("npx", options.args, { cwd: options.context.runtime.cwd, env: { @@ -531,7 +624,9 @@ async function runPrismaCommand(options: { ...options.env, }, signal: options.context.runtime.signal, - stdio: shouldPipeOutput ? ["ignore", "pipe", "pipe"] : ["ignore", "ignore", "ignore"], + stdio: shouldPipeOutput + ? ["ignore", "pipe", "pipe"] + : ["ignore", "ignore", "ignore"], }); if (shouldPipeOutput) { @@ -539,16 +634,23 @@ async function runPrismaCommand(options: { child.stderr?.pipe(options.context.output.stderr, { end: false }); } - const exit = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => { + const exit = await new Promise<{ + code: number | null; + signal: NodeJS.Signals | null; + }>((resolve, reject) => { child.once("error", reject); child.once("close", (code, signal) => resolve({ code, signal })); }); if (exit.signal) { - throw new Error(`${options.displayCommand} was terminated by ${exit.signal}.`); + throw new Error( + `${options.displayCommand} was terminated by ${exit.signal}.`, + ); } if (exit.code !== 0) { - throw new Error(`${options.displayCommand} exited with code ${exit.code ?? 1}.`); + throw new Error( + `${options.displayCommand} exited with code ${exit.code ?? 1}.`, + ); } } diff --git a/packages/cli/src/lib/app/bun-project.ts b/packages/cli/src/lib/app/bun-project.ts index 5c5cf0f..bba2bf0 100644 --- a/packages/cli/src/lib/app/bun-project.ts +++ b/packages/cli/src/lib/app/bun-project.ts @@ -10,7 +10,10 @@ export interface BunPackageJsonLike { devDependencies?: unknown; } -export async function readBunPackageJson(appPath: string, signal?: AbortSignal): Promise { +export async function readBunPackageJson( + appPath: string, + signal?: AbortSignal, +): Promise { const packageJsonPath = path.join(appPath, "package.json"); let content: string; @@ -36,7 +39,9 @@ export async function readBunPackageJson(appPath: string, signal?: AbortSignal): } } -export function readBunPackageEntrypoint(packageJson: BunPackageJsonLike | null): string | undefined { +export function readBunPackageEntrypoint( + packageJson: BunPackageJsonLike | null, +): string | undefined { if (typeof packageJson?.main === "string") { return packageJson.main; } @@ -57,7 +62,9 @@ export async function resolveBunEntrypoint( const candidate = explicitEntrypoint ?? readBunPackageEntrypoint(packageJson); if (!candidate) { - throw new Error("Entrypoint is required. Pass --entry or define package.json main or module."); + throw new Error( + "Entrypoint is required. Pass --entry or define package.json main or module.", + ); } if (path.isAbsolute(candidate)) { diff --git a/packages/cli/src/lib/app/deploy-output.ts b/packages/cli/src/lib/app/deploy-output.ts index ffb780c..0b129a2 100644 --- a/packages/cli/src/lib/app/deploy-output.ts +++ b/packages/cli/src/lib/app/deploy-output.ts @@ -15,13 +15,22 @@ const DEPLOY_OUTPUT_MIN_LABEL_WIDTH = "Framework".length; const DEPLOY_OUTPUT_MIN_VALUE_WIDTH = "HTTP 3000".length; const DEPLOY_SETTINGS_MIN_KEY_WIDTH = "framework:".length; -export function renderDeployOutputRows(ui: ShellUi, rows: DeployOutputRow[]): string[] { +export function renderDeployOutputRows( + ui: ShellUi, + rows: DeployOutputRow[], +): string[] { if (rows.length === 0) { return []; } - const labelWidth = Math.max(DEPLOY_OUTPUT_MIN_LABEL_WIDTH, ...rows.map((row) => row.label.length)); - const valueWidth = Math.max(DEPLOY_OUTPUT_MIN_VALUE_WIDTH, ...rows.map((row) => row.value?.length ?? 0)); + const labelWidth = Math.max( + DEPLOY_OUTPUT_MIN_LABEL_WIDTH, + ...rows.map((row) => row.label.length), + ); + const valueWidth = Math.max( + DEPLOY_OUTPUT_MIN_VALUE_WIDTH, + ...rows.map((row) => row.value?.length ?? 0), + ); return rows.map((row) => { if (!row.value) { @@ -35,12 +44,18 @@ export function renderDeployOutputRows(ui: ShellUi, rows: DeployOutputRow[]): st }); } -export function renderDeploySettingsPreview(ui: ShellUi, rows: DeploySettingsPreviewRow[]): string[] { +export function renderDeploySettingsPreview( + ui: ShellUi, + rows: DeploySettingsPreviewRow[], +): string[] { if (rows.length === 0) { return []; } - const keyWidth = Math.max(DEPLOY_SETTINGS_MIN_KEY_WIDTH, ...rows.map((row) => `${row.key}:`.length)); + const keyWidth = Math.max( + DEPLOY_SETTINGS_MIN_KEY_WIDTH, + ...rows.map((row) => `${row.key}:`.length), + ); const rail = ui.dim("│"); return rows.map((row) => { diff --git a/packages/cli/src/lib/app/domain-guidance.ts b/packages/cli/src/lib/app/domain-guidance.ts index 762dcf7..edcea8f 100644 --- a/packages/cli/src/lib/app/domain-guidance.ts +++ b/packages/cli/src/lib/app/domain-guidance.ts @@ -13,7 +13,9 @@ interface DomainFailureGuidanceInput { dnsRecords: DomainDnsRecord[]; } -export function formatDomainFailureFix(domain: DomainFailureGuidanceInput): string | null { +export function formatDomainFailureFix( + domain: DomainFailureGuidanceInput, +): string | null { if (domain.status !== "failed") { return null; } diff --git a/packages/cli/src/lib/app/env-config.ts b/packages/cli/src/lib/app/env-config.ts index 4098c41..2da0451 100644 --- a/packages/cli/src/lib/app/env-config.ts +++ b/packages/cli/src/lib/app/env-config.ts @@ -90,7 +90,9 @@ export function parseKeyValuePositional( `prisma-cli project env ${command} requires KEY=VALUE`, "No KEY=VALUE positional argument was supplied.", "Pass the variable as KEY=VALUE, e.g. STRIPE_KEY=sk_test_xxx.", - [`prisma-cli project env ${command} STRIPE_KEY=sk_test_xxx --role production`], + [ + `prisma-cli project env ${command} STRIPE_KEY=sk_test_xxx --role production`, + ], "app", ); } @@ -120,7 +122,9 @@ export function parseKeyValuePositional( `KEY=VALUE argument is missing the = separator`, `"${raw}" does not contain an = character.`, "Pass the variable as KEY=VALUE, e.g. STRIPE_KEY=sk_test_xxx.", - [`prisma-cli project env ${command} STRIPE_KEY=sk_test_xxx --role production`], + [ + `prisma-cli project env ${command} STRIPE_KEY=sk_test_xxx --role production`, + ], "app", ); } @@ -154,7 +158,9 @@ export function validateKey( `Variable key cannot be empty`, "An empty key was passed.", "Pass an env-var key, e.g. STRIPE_KEY.", - [`prisma-cli project env ${command} STRIPE_KEY${command === "remove" ? "" : "=value"} --role production`], + [ + `prisma-cli project env ${command} STRIPE_KEY${command === "remove" ? "" : "=value"} --role production`, + ], "app", ); } @@ -174,7 +180,9 @@ export function validateKey( `Variable key "${key}" must match the POSIX env-var shape`, "Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores.", "Rename the key to match [A-Z_][A-Z0-9_]*.", - [`prisma-cli project env ${command} STRIPE_KEY${command === "remove" ? "" : "=value"} --role production`], + [ + `prisma-cli project env ${command} STRIPE_KEY${command === "remove" ? "" : "=value"} --role production`, + ], "app", ); } diff --git a/packages/cli/src/lib/app/env-file.ts b/packages/cli/src/lib/app/env-file.ts index 3e2687c..755ddaa 100644 --- a/packages/cli/src/lib/app/env-file.ts +++ b/packages/cli/src/lib/app/env-file.ts @@ -127,7 +127,7 @@ function extractParsedKeys(contents: string): ParsedEnvFileKey[] { const valueStart = line.slice(match[0].length).trimStart(); const openingQuote = valueStart[0]; if ( - (openingQuote === "\"" || openingQuote === "'" || openingQuote === "`") && + (openingQuote === '"' || openingQuote === "'" || openingQuote === "`") && !hasClosingQuote(valueStart, openingQuote, 1) ) { multilineQuote = openingQuote; @@ -146,9 +146,10 @@ function validateEnvFileKey( try { validateKey(key, command === "deploy" ? "add" : command); } catch (error) { - const reason = error instanceof Error && error.message.length > 0 - ? error.message - : "Invalid environment variable key."; + const reason = + error instanceof Error && error.message.length > 0 + ? error.message + : "Invalid environment variable key."; throw usageError( `Invalid environment variable "${key}" in "${filePath}"`, `Line ${line}: ${reason}`, @@ -159,7 +160,11 @@ function validateEnvFileKey( } } -function hasClosingQuote(value: string, quote: string, startIndex: number): boolean { +function hasClosingQuote( + value: string, + quote: string, + startIndex: number, +): boolean { for (let index = startIndex; index < value.length; index += 1) { if (value[index] === quote && !isEscaped(value, index)) { return true; @@ -170,7 +175,11 @@ function hasClosingQuote(value: string, quote: string, startIndex: number): bool function isEscaped(value: string, index: number): boolean { let backslashes = 0; - for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) { + for ( + let cursor = index - 1; + cursor >= 0 && value[cursor] === "\\"; + cursor -= 1 + ) { backslashes += 1; } return backslashes % 2 === 1; diff --git a/packages/cli/src/lib/app/env-vars.ts b/packages/cli/src/lib/app/env-vars.ts index d27c53c..41b2a23 100644 --- a/packages/cli/src/lib/app/env-vars.ts +++ b/packages/cli/src/lib/app/env-vars.ts @@ -18,7 +18,9 @@ export function parseEnvAssignments( "At least one environment variable is required", `prisma-cli app ${options.commandName} needs at least one --env NAME=VALUE flag in the current mode.`, `Pass one or more --env NAME=VALUE flags, for example prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example.`, - [`prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`], + [ + `prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`, + ], "app", ); } @@ -33,7 +35,9 @@ export function parseEnvAssignments( "Environment variable assignment must use NAME=VALUE", "A provided --env flag is missing the = separator.", `Pass repeated --env NAME=VALUE flags, for example prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example.`, - [`prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`], + [ + `prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`, + ], "app", ); } @@ -44,7 +48,9 @@ export function parseEnvAssignments( "Environment variable name is required", "A provided --env flag has an empty variable name.", `Pass repeated --env NAME=VALUE flags, for example prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example.`, - [`prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`], + [ + `prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`, + ], "app", ); } @@ -92,22 +98,32 @@ export async function parseEnvInputs( continue; } - const fileAssignments = await readEnvFileAssignments(cwd, value, options.commandName); + const fileAssignments = await readEnvFileAssignments( + cwd, + value, + options.commandName, + ); expandedAssignments.push( - ...fileAssignments.map((assignment) => `${assignment.key}=${assignment.value}`), + ...fileAssignments.map( + (assignment) => `${assignment.key}=${assignment.value}`, + ), ); } return parseEnvAssignments(expandedAssignments, options); } -function validateEnvAssignmentName(name: string, commandName: EnvAssignmentOptions["commandName"]): void { +function validateEnvAssignmentName( + name: string, + commandName: EnvAssignmentOptions["commandName"], +): void { try { validateKey(name, "add"); } catch (error) { - const reason = error instanceof Error && error.message.length > 0 - ? error.message - : "Invalid environment variable name."; + const reason = + error instanceof Error && error.message.length > 0 + ? error.message + : "Invalid environment variable name."; throw usageError( `Invalid environment variable "${name}"`, reason, diff --git a/packages/cli/src/lib/app/local-dev.ts b/packages/cli/src/lib/app/local-dev.ts index 63ef6c7..bec932b 100644 --- a/packages/cli/src/lib/app/local-dev.ts +++ b/packages/cli/src/lib/app/local-dev.ts @@ -2,10 +2,20 @@ import { spawn, type SpawnOptions } from "node:child_process"; import { access } from "node:fs/promises"; import path from "node:path"; -import type { PreviewBuildType, ResolvedPreviewBuildType } from "./preview-build"; -import { readBunPackageEntrypoint, readBunPackageJson, resolveBunEntrypoint } from "./bun-project"; +import type { + PreviewBuildType, + ResolvedPreviewBuildType, +} from "./preview-build"; +import { + readBunPackageEntrypoint, + readBunPackageJson, + resolveBunEntrypoint, +} from "./bun-project"; -export type LocalBuildType = Extract; +export type LocalBuildType = Extract< + ResolvedPreviewBuildType, + "bun" | "nextjs" +>; const NEXT_CONFIG_FILENAMES = [ "next.config.js", @@ -47,7 +57,10 @@ export async function resolveLocalBuildType( return detectLocalBuildType(appPath, signal); } -export async function detectLocalBuildType(appPath: string, signal?: AbortSignal): Promise { +export async function detectLocalBuildType( + appPath: string, + signal?: AbortSignal, +): Promise { if (await isNextProject(appPath, signal)) { return "nextjs"; } @@ -117,7 +130,11 @@ export async function runLocalApp(options: { }; } - const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint, options.signal); + const entrypoint = await resolveBunEntrypoint( + options.appPath, + options.entrypoint, + options.signal, + ); const command = await runWithFallback( [ { @@ -148,7 +165,10 @@ export async function runLocalApp(options: { }; } -async function isNextProject(appPath: string, signal?: AbortSignal): Promise { +async function isNextProject( + appPath: string, + signal?: AbortSignal, +): Promise { for (const fileName of NEXT_CONFIG_FILENAMES) { signal?.throwIfAborted(); try { @@ -166,7 +186,10 @@ async function isNextProject(appPath: string, signal?: AbortSignal): Promise { +async function isBunProject( + appPath: string, + signal?: AbortSignal, +): Promise { signal?.throwIfAborted(); try { // access does not accept AbortSignal; check before and after the filesystem boundary. @@ -194,12 +217,18 @@ async function isBunProject(appPath: string, signal?: AbortSignal): Promise typeof value === "string" && /\bbun\b/.test(value)); + const hasEntrypoint = + typeof readBunPackageEntrypoint(packageJson) === "string"; + const hasBunDependency = + hasDependency(packageJson, "@types/bun") || + hasDependency(packageJson, "bun"); + const scriptValues = + typeof packageJson.scripts === "object" && packageJson.scripts !== null + ? Object.values(packageJson.scripts) + : []; + const usesBunScripts = scriptValues.some( + (value) => typeof value === "string" && /\bbun\b/.test(value), + ); return hasEntrypoint && (hasBunDependency || usesBunScripts); } @@ -212,10 +241,14 @@ function hasDependency( return false; } - const dependencyGroups = [packageJson.dependencies, packageJson.devDependencies]; + const dependencyGroups = [ + packageJson.dependencies, + packageJson.devDependencies, + ]; return dependencyGroups.some( - (group) => typeof group === "object" && group !== null && dependencyName in group, + (group) => + typeof group === "object" && group !== null && dependencyName in group, ); } async function runWithFallback( diff --git a/packages/cli/src/lib/app/preview-branch-database.ts b/packages/cli/src/lib/app/preview-branch-database.ts index db8c377..a19e02d 100644 --- a/packages/cli/src/lib/app/preview-branch-database.ts +++ b/packages/cli/src/lib/app/preview-branch-database.ts @@ -69,7 +69,11 @@ export async function createBranchDatabase( }); if (result.error || !result.data) { - throw apiCallError(`Failed to create database for branch "${options.branchName}"`, result.response, result.error); + throw apiCallError( + `Failed to create database for branch "${options.branchName}"`, + result.response, + result.error, + ); } return normalizeBranchDatabaseRecord(result.data.data as RawDatabaseRecord); @@ -103,10 +107,14 @@ export async function listEnvironmentVariables( signal: options.signal, }); if (result.error || !result.data) { - throw apiCallError("Failed to list environment variables", result.response, result.error); + throw apiCallError( + "Failed to list environment variables", + result.response, + result.error, + ); } - variables.push(...result.data.data as RawEnvironmentVariableRecord[]); + variables.push(...(result.data.data as RawEnvironmentVariableRecord[])); if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) { break; @@ -140,10 +148,16 @@ export async function createEnvironmentVariable( }); if (result.error || !result.data) { - throw apiCallError(`Failed to add ${options.key}`, result.response, result.error); + throw apiCallError( + `Failed to add ${options.key}`, + result.response, + result.error, + ); } - return normalizeEnvironmentVariable(result.data.data as RawEnvironmentVariableRecord); + return normalizeEnvironmentVariable( + result.data.data as RawEnvironmentVariableRecord, + ); } export async function deleteBranchDatabase( @@ -161,7 +175,11 @@ export async function deleteBranchDatabase( }); if (result.error) { - throw apiCallError("Failed to delete branch database", result.response, result.error); + throw apiCallError( + "Failed to delete branch database", + result.response, + result.error, + ); } } @@ -184,10 +202,16 @@ export async function updateEnvironmentVariable( }); if (result.error || !result.data) { - throw apiCallError("Failed to update environment variable", result.response, result.error); + throw apiCallError( + "Failed to update environment variable", + result.response, + result.error, + ); } - return normalizeEnvironmentVariable(result.data.data as RawEnvironmentVariableRecord); + return normalizeEnvironmentVariable( + result.data.data as RawEnvironmentVariableRecord, + ); } export async function deleteEnvironmentVariable( @@ -205,11 +229,17 @@ export async function deleteEnvironmentVariable( }); if (result.error) { - throw apiCallError("Failed to delete environment variable", result.response, result.error); + throw apiCallError( + "Failed to delete environment variable", + result.response, + result.error, + ); } } -function normalizeEnvironmentVariable(variable: RawEnvironmentVariableRecord): PreviewEnvironmentVariableRecord { +function normalizeEnvironmentVariable( + variable: RawEnvironmentVariableRecord, +): PreviewEnvironmentVariableRecord { return { id: variable.id, key: variable.key, @@ -219,13 +249,17 @@ function normalizeEnvironmentVariable(variable: RawEnvironmentVariableRecord): P }; } -function normalizeBranchDatabaseRecord(database: RawDatabaseRecord): PreviewBranchDatabaseRecord { +function normalizeBranchDatabaseRecord( + database: RawDatabaseRecord, +): PreviewBranchDatabaseRecord { const connection = database.connections?.[0]; const databaseUrl = connection?.endpoints?.pooled?.connectionString; const directUrl = connection?.endpoints?.direct?.connectionString ?? null; if (!databaseUrl) { - throw new Error("Created database did not return a pooled connection string."); + throw new Error( + "Created database did not return a pooled connection string.", + ); } return { @@ -246,7 +280,8 @@ function apiCallError( return new Error("Resource Not Found"); } - const message = error.error?.message ?? `Management API returned HTTP ${response.status}.`; + const message = + error.error?.message ?? `Management API returned HTTP ${response.status}.`; const hint = error.error?.hint ? ` ${error.error.hint}` : ""; return new Error(`${summary}: ${message}${hint}`); } diff --git a/packages/cli/src/lib/app/preview-build-settings.ts b/packages/cli/src/lib/app/preview-build-settings.ts index 2ce7970..2b6d57d 100644 --- a/packages/cli/src/lib/app/preview-build-settings.ts +++ b/packages/cli/src/lib/app/preview-build-settings.ts @@ -9,10 +9,14 @@ import { readBunPackageJson, type BunPackageJsonLike } from "./bun-project"; import type { ResolvedPreviewBuildType } from "./preview-build"; type PackageManager = "bun" | "pnpm" | "yarn" | "npm"; -export type PreviewBuildSettingsBuildType = Extract; +export type PreviewBuildSettingsBuildType = Extract< + ResolvedPreviewBuildType, + "nextjs" | "tanstack-start" | "bun" +>; export const PRISMA_APP_CONFIG_FILENAME = "prisma.app.json"; -export const PRISMA_APP_CONFIG_SCHEMA_URL = "https://pris.ly/schemas/prisma-app-config.v1.json"; +export const PRISMA_APP_CONFIG_SCHEMA_URL = + "https://pris.ly/schemas/prisma-app-config.v1.json"; interface ResolvedBuildCommand { command: string | null; @@ -44,7 +48,10 @@ export async function resolveOrCreatePreviewBuildSettings(options: { signal?: AbortSignal; }): Promise { const configPath = path.join(options.appPath, PRISMA_APP_CONFIG_FILENAME); - const existing = await readPreviewBuildSettingsConfig(configPath, options.signal); + const existing = await readPreviewBuildSettingsConfig( + configPath, + options.signal, + ); if (existing) { return { status: "used", @@ -74,7 +81,10 @@ export async function resolveOrCreatePreviewBuildSettings(options: { }); } catch (error) { if ((error as NodeJS.ErrnoException).code === "EEXIST") { - const raced = await readPreviewBuildSettingsConfig(configPath, options.signal); + const raced = await readPreviewBuildSettingsConfig( + configPath, + options.signal, + ); if (raced) { return { status: "used", @@ -108,27 +118,45 @@ export async function resolvePreviewBuildSettings(options: { }): Promise { switch (options.buildType) { case "nextjs": { - const packageJson = await readBunPackageJson(options.appPath, options.signal); - const buildCommand = await resolveFrameworkBuildCommand(options.appPath, packageJson, { - command: "next build", - source: "Next.js default", - signal: options.signal, - }); - const outputRoot = await resolveNextOutputRoot(options.appPath, options.signal); + const packageJson = await readBunPackageJson( + options.appPath, + options.signal, + ); + const buildCommand = await resolveFrameworkBuildCommand( + options.appPath, + packageJson, + { + command: "next build", + source: "Next.js default", + signal: options.signal, + }, + ); + const outputRoot = await resolveNextOutputRoot( + options.appPath, + options.signal, + ); return { buildCommand: buildCommand.command, buildCommandSource: buildCommand.source, outputDirectory: joinPosix(outputRoot, "standalone"), - outputDirectorySource: outputRoot === ".next" ? "Next.js output" : "next.config distDir", + outputDirectorySource: + outputRoot === ".next" ? "Next.js output" : "next.config distDir", }; } case "tanstack-start": { - const packageJson = await readBunPackageJson(options.appPath, options.signal); - const buildCommand = await resolveFrameworkBuildCommand(options.appPath, packageJson, { - command: "vite build", - source: "TanStack Start default", - signal: options.signal, - }); + const packageJson = await readBunPackageJson( + options.appPath, + options.signal, + ); + const buildCommand = await resolveFrameworkBuildCommand( + options.appPath, + packageJson, + { + command: "vite build", + source: "TanStack Start default", + signal: options.signal, + }, + ); return { buildCommand: buildCommand.command, buildCommandSource: buildCommand.source, @@ -137,12 +165,19 @@ export async function resolvePreviewBuildSettings(options: { }; } case "bun": { - const packageJson = await readBunPackageJson(options.appPath, options.signal); - const buildCommand = await resolveFrameworkBuildCommand(options.appPath, packageJson, { - command: null, - source: null, - signal: options.signal, - }); + const packageJson = await readBunPackageJson( + options.appPath, + options.signal, + ); + const buildCommand = await resolveFrameworkBuildCommand( + options.appPath, + packageJson, + { + command: null, + source: null, + signal: options.signal, + }, + ); return { buildCommand: buildCommand.command, buildCommandSource: buildCommand.source, @@ -184,7 +219,10 @@ async function readPreviewBuildSettingsConfig( } if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - throw invalidPrismaAppConfigError(configPath, "The file must contain a JSON object."); + throw invalidPrismaAppConfigError( + configPath, + "The file must contain a JSON object.", + ); } const raw = parsed as Record; @@ -193,22 +231,34 @@ async function readPreviewBuildSettingsConfig( raw.$schema !== undefined && typeof raw.$schema !== "string" ) { - throw invalidPrismaAppConfigError(configPath, "The $schema field must be a string when present."); + throw invalidPrismaAppConfigError( + configPath, + "The $schema field must be a string when present.", + ); } if (raw.buildCommand !== null && typeof raw.buildCommand !== "string") { - throw invalidPrismaAppConfigError(configPath, "The buildCommand field must be a string or null."); + throw invalidPrismaAppConfigError( + configPath, + "The buildCommand field must be a string or null.", + ); } let buildCommand: string | null = null; if (typeof raw.buildCommand === "string") { buildCommand = raw.buildCommand.trim(); if (buildCommand.length === 0) { - throw invalidPrismaAppConfigError(configPath, "The buildCommand field must not be an empty string. Use null to skip the build step."); + throw invalidPrismaAppConfigError( + configPath, + "The buildCommand field must not be an empty string. Use null to skip the build step.", + ); } } - const outputDirectory = normalizeConfigOutputDirectory(configPath, raw.outputDirectory); + const outputDirectory = normalizeConfigOutputDirectory( + configPath, + raw.outputDirectory, + ); return { buildCommand, @@ -216,9 +266,15 @@ async function readPreviewBuildSettingsConfig( }; } -function normalizeConfigOutputDirectory(configPath: string, value: unknown): string { +function normalizeConfigOutputDirectory( + configPath: string, + value: unknown, +): string { if (typeof value !== "string" || value.trim().length === 0) { - throw invalidPrismaAppConfigError(configPath, "The outputDirectory field must be a non-empty string."); + throw invalidPrismaAppConfigError( + configPath, + "The outputDirectory field must be a non-empty string.", + ); } const normalized = normalizeRelativePath(value); @@ -232,15 +288,19 @@ function normalizeConfigOutputDirectory(configPath: string, value: unknown): str return normalized; } -function invalidPrismaAppConfigError(configPath: string, why: string): CliError { +function invalidPrismaAppConfigError( + configPath: string, + why: string, +): CliError { return new CliError({ code: "APP_CONFIG_INVALID", domain: "app", summary: `Invalid ${PRISMA_APP_CONFIG_FILENAME}`, why, - fix: `Edit ${PRISMA_APP_CONFIG_FILENAME} so buildCommand is a string or null ` - + "and outputDirectory is a relative path inside the app root. " - + "Delete the file and rerun prisma-cli app deploy to regenerate defaults.", + fix: + `Edit ${PRISMA_APP_CONFIG_FILENAME} so buildCommand is a string or null ` + + "and outputDirectory is a relative path inside the app root. " + + "Delete the file and rerun prisma-cli app deploy to regenerate defaults.", where: configPath, meta: { configPath, @@ -250,7 +310,11 @@ function invalidPrismaAppConfigError(configPath: string, why: string): CliError }); } -export async function hasRootFile(appPath: string, filenames: readonly string[], signal?: AbortSignal): Promise { +export async function hasRootFile( + appPath: string, + filenames: readonly string[], + signal?: AbortSignal, +): Promise { let entries: string[]; try { signal?.throwIfAborted(); @@ -264,16 +328,25 @@ export async function hasRootFile(appPath: string, filenames: readonly string[], return entries.some((entry) => filenames.includes(entry)); } -export function hasPackageDependency(packageJson: BunPackageJsonLike | null, dependencyName: string): boolean { +export function hasPackageDependency( + packageJson: BunPackageJsonLike | null, + dependencyName: string, +): boolean { return hasAnyPackageDependency(packageJson, [dependencyName]); } -export function hasAnyPackageDependency(packageJson: BunPackageJsonLike | null, dependencyNames: readonly string[]): boolean { +export function hasAnyPackageDependency( + packageJson: BunPackageJsonLike | null, + dependencyNames: readonly string[], +): boolean { if (!packageJson) { return false; } - const dependencyGroups = [packageJson.dependencies, packageJson.devDependencies]; + const dependencyGroups = [ + packageJson.dependencies, + packageJson.devDependencies, + ]; return dependencyGroups.some((group) => { if (!group || typeof group !== "object") { return false; @@ -294,7 +367,11 @@ async function resolveFrameworkBuildCommand( ): Promise { const buildScript = readBuildScript(packageJson); if (buildScript) { - const packageManager = await resolvePackageManager(appPath, packageJson, fallback.signal); + const packageManager = await resolvePackageManager( + appPath, + packageJson, + fallback.signal, + ); if (!packageManager) { return { command: buildScript, @@ -314,7 +391,9 @@ async function resolveFrameworkBuildCommand( }; } -function readBuildScript(packageJson: BunPackageJsonLike | null): string | null { +function readBuildScript( + packageJson: BunPackageJsonLike | null, +): string | null { if (!packageJson?.scripts || typeof packageJson.scripts !== "object") { return null; } @@ -333,12 +412,17 @@ async function resolvePackageManager( packageJson: BunPackageJsonLike | null, signal?: AbortSignal, ): Promise { - const fromPackageManager = packageManagerFromPackageJson(packageJson?.packageManager); + const fromPackageManager = packageManagerFromPackageJson( + packageJson?.packageManager, + ); if (fromPackageManager) { return fromPackageManager; } - if (await pathExists(path.join(appPath, "bun.lock"), signal) || await pathExists(path.join(appPath, "bun.lockb"), signal)) { + if ( + (await pathExists(path.join(appPath, "bun.lock"), signal)) || + (await pathExists(path.join(appPath, "bun.lockb"), signal)) + ) { return "bun"; } @@ -361,7 +445,9 @@ function packageManagerFromPackageJson(value: unknown): PackageManager | null { } const name = value.split("@")[0]; - return name === "bun" || name === "pnpm" || name === "yarn" || name === "npm" ? name : null; + return name === "bun" || name === "pnpm" || name === "yarn" || name === "npm" + ? name + : null; } export async function runResolvedBuildCommand( @@ -384,26 +470,33 @@ function execBuildCommand( signal?: AbortSignal, ): Promise { return new Promise((resolve, reject) => { - const child = exec(command, { - cwd, - env: { - ...process.env, - PATH: [ - path.join(cwd, "node_modules", ".bin"), - process.env.PATH, - ].filter(Boolean).join(path.delimiter), + const child = exec( + command, + { + cwd, + env: { + ...process.env, + PATH: [path.join(cwd, "node_modules", ".bin"), process.env.PATH] + .filter(Boolean) + .join(path.delimiter), + }, + maxBuffer: 10 * 1024 * 1024, + signal, }, - maxBuffer: 10 * 1024 * 1024, - signal, - }, (error, stdout, stderr) => { - if (error) { - const output = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n"); - reject(new Error(`${failurePrefix} failed:\n${output || error.message}`)); - return; - } - - resolve(); - }); + (error, stdout, stderr) => { + if (error) { + const output = [stderr.trim(), stdout.trim()] + .filter(Boolean) + .join("\n"); + reject( + new Error(`${failurePrefix} failed:\n${output || error.message}`), + ); + return; + } + + resolve(); + }, + ); if (signal?.aborted) { child.kill(); @@ -411,12 +504,18 @@ function execBuildCommand( }); } -async function resolveNextOutputRoot(appPath: string, signal?: AbortSignal): Promise { +async function resolveNextOutputRoot( + appPath: string, + signal?: AbortSignal, +): Promise { const config = await readNextConfig(appPath, signal); return config.distDir ?? ".next"; } -async function readNextConfig(appPath: string, signal?: AbortSignal): Promise { +async function readNextConfig( + appPath: string, + signal?: AbortSignal, +): Promise { for (const fileName of NEXT_CONFIG_FILENAMES) { const filePath = path.join(appPath, fileName); let content: string; @@ -447,8 +546,12 @@ function readStaticNextConfig(content: string): StaticNextConfig { try { const module = parseModule(content); const program = asAstNode(module.$ast); - const bindings = program ? collectStaticBindings(program) : new Map(); - const configObject = program ? findExportedConfigObject(program, bindings) : null; + const bindings = program + ? collectStaticBindings(program) + : new Map(); + const configObject = program + ? findExportedConfigObject(program, bindings) + : null; if (!configObject) { return {}; } @@ -459,7 +562,8 @@ function readStaticNextConfig(content: string): StaticNextConfig { return { distDir, - output: output === "standalone" || output === "export" ? output : undefined, + output: + output === "standalone" || output === "export" ? output : undefined, }; } catch { return {}; @@ -470,7 +574,9 @@ export function joinPosix(...parts: string[]): string { return parts.join("/").replace(/\/+/g, "/"); } -export function nextOutputRootFromStandaloneDirectory(outputDirectory: string): string { +export function nextOutputRootFromStandaloneDirectory( + outputDirectory: string, +): string { const normalized = outputDirectory.replace(/\/+$/g, ""); if (normalized === "standalone") { return "."; @@ -493,7 +599,7 @@ function asAstNode(value: unknown): AstNode | null { } const type = (value as { type?: unknown }).type; - return typeof type === "string" ? value as AstNode : null; + return typeof type === "string" ? (value as AstNode) : null; } function astNodes(value: unknown): AstNode[] { @@ -523,7 +629,10 @@ function collectStaticBindings(program: AstNode): Map { return bindings; } -function findExportedConfigObject(program: AstNode, bindings: Map): AstNode | null { +function findExportedConfigObject( + program: AstNode, + bindings: Map, +): AstNode | null { for (const statement of astNodes(program.body)) { if (statement.type === "ExportDefaultDeclaration") { return resolveConfigObject(statement.declaration, bindings); @@ -534,7 +643,10 @@ function findExportedConfigObject(program: AstNode, bindings: Map { +async function pathExists( + targetPath: string, + signal?: AbortSignal, +): Promise { try { signal?.throwIfAborted(); await stat(targetPath); diff --git a/packages/cli/src/lib/app/preview-build.ts b/packages/cli/src/lib/app/preview-build.ts index 3a39788..6eb2662 100644 --- a/packages/cli/src/lib/app/preview-build.ts +++ b/packages/cli/src/lib/app/preview-build.ts @@ -1,4 +1,17 @@ -import { chmod, copyFile, cp, lstat, mkdir, mkdtemp, readdir, readFile, readlink, rm, stat, writeFile } from "node:fs/promises"; +import { + chmod, + copyFile, + cp, + lstat, + mkdir, + mkdtemp, + readdir, + readFile, + readlink, + rm, + stat, + writeFile, +} from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -41,7 +54,7 @@ export const PREVIEW_BUILD_TYPES = [ "tanstack-start", ] as const; -export type PreviewBuildType = typeof PREVIEW_BUILD_TYPES[number]; +export type PreviewBuildType = (typeof PREVIEW_BUILD_TYPES)[number]; export type ResolvedPreviewBuildType = Exclude; export const RESOLVED_PREVIEW_BUILD_TYPES = PREVIEW_BUILD_TYPES.filter( @@ -114,7 +127,11 @@ export async function executePreviewBuild(options: { const artifact = await strategy.execute(options.signal); try { - await normalizeArtifactSymlinks(artifact.directory, options.appPath, options.signal); + await normalizeArtifactSymlinks( + artifact.directory, + options.appPath, + options.signal, + ); return { artifact, buildType, @@ -191,15 +208,25 @@ async function createPreviewBuildStrategy(options: { }): Promise { switch (options.buildType) { case "nextjs": - return new PreviewNextjsBuild({ appPath: options.appPath, buildSettings: options.buildSettings }); + return new PreviewNextjsBuild({ + appPath: options.appPath, + buildSettings: options.buildSettings, + }); case "nuxt": return new NuxtBuild({ appPath: options.appPath }); case "astro": return new AstroBuild({ appPath: options.appPath }); case "tanstack-start": - return new PreviewTanstackStartBuild({ appPath: options.appPath, buildSettings: options.buildSettings }); + return new PreviewTanstackStartBuild({ + appPath: options.appPath, + buildSettings: options.buildSettings, + }); case "bun": { - const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint, options.signal); + const entrypoint = await resolveBunEntrypoint( + options.appPath, + options.entrypoint, + options.signal, + ); return new PreviewBunBuild({ appPath: options.appPath, strategy: new BunBuild({ @@ -216,33 +243,43 @@ class PreviewNextjsBuild implements BuildStrategy { readonly #appPath: string; readonly #buildSettings?: PreviewBuildSettings; - constructor(options: { appPath: string; buildSettings?: PreviewBuildSettings }) { + constructor(options: { + appPath: string; + buildSettings?: PreviewBuildSettings; + }) { this.#appPath = options.appPath; this.#buildSettings = options.buildSettings; } async canBuild(signal?: AbortSignal): Promise { const packageJson = await readBunPackageJson(this.#appPath, signal); - return (await hasRootFile(this.#appPath, NEXT_CONFIG_FILENAMES, signal)) || hasPackageDependency(packageJson, "next"); + return ( + (await hasRootFile(this.#appPath, NEXT_CONFIG_FILENAMES, signal)) || + hasPackageDependency(packageJson, "next") + ); } async execute(signal?: AbortSignal): Promise { - const settings = this.#buildSettings ?? await resolvePreviewBuildSettings({ - appPath: this.#appPath, - buildType: "nextjs", - signal, - }); + const settings = + this.#buildSettings ?? + (await resolvePreviewBuildSettings({ + appPath: this.#appPath, + buildType: "nextjs", + signal, + })); await runResolvedBuildCommand(this.#appPath, settings, "Next.js", signal); const standaloneDir = path.join(this.#appPath, settings.outputDirectory); - if (!await directoryExists(standaloneDir, signal)) { + if (!(await directoryExists(standaloneDir, signal))) { // No `output: "standalone"` in next.config: the build itself // succeeded, so package the full tree and serve with `next start` // instead of failing. Bigger artifact, same running app. return stageNextjsFullTreeFallbackArtifact(this.#appPath, signal); } - const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-"))); + const outDir = await unsupportedFilesystemBoundary(signal, () => + mkdtemp(path.join(os.tmpdir(), "compute-build-")), + ); try { const artifactDir = path.join(outDir, "app"); @@ -252,11 +289,16 @@ class PreviewNextjsBuild implements BuildStrategy { appPath: this.#appPath, signal, }); - const entrypoint = await findNextStandaloneEntrypoint(artifactDir, signal); + const entrypoint = await findNextStandaloneEntrypoint( + artifactDir, + signal, + ); await copyNextjsStaticAssets({ appPath: this.#appPath, artifactDir, - outputRoot: nextOutputRootFromStandaloneDirectory(settings.outputDirectory), + outputRoot: nextOutputRootFromStandaloneDirectory( + settings.outputDirectory, + ), entrypoint, signal, }); @@ -288,8 +330,13 @@ const FULL_TREE_NEXT_START_SOURCE = [ "", ].join("\n"); -async function stageNextjsFullTreeFallbackArtifact(appPath: string, signal?: AbortSignal): Promise { - const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-"))); +async function stageNextjsFullTreeFallbackArtifact( + appPath: string, + signal?: AbortSignal, +): Promise { + const outDir = await unsupportedFilesystemBoundary(signal, () => + mkdtemp(path.join(os.tmpdir(), "compute-build-")), + ); try { const artifactDir = path.join(outDir, "app"); @@ -328,9 +375,7 @@ async function stageNextjsFullTreeFallbackArtifact(appPath: string, signal?: Abo /** Excludes VCS internals and dotenv files (local secrets, superseded by the deploy env). */ function isExcludedFromFullTreeArtifact(basename: string): boolean { return ( - basename === ".git" || - basename === ".env" || - basename.startsWith(".env.") + basename === ".git" || basename === ".env" || basename.startsWith(".env.") ); } @@ -338,7 +383,10 @@ class PreviewTanstackStartBuild implements BuildStrategy { readonly #appPath: string; readonly #buildSettings?: PreviewBuildSettings; - constructor(options: { appPath: string; buildSettings?: PreviewBuildSettings }) { + constructor(options: { + appPath: string; + buildSettings?: PreviewBuildSettings; + }) { this.#appPath = options.appPath; this.#buildSettings = options.buildSettings; } @@ -349,32 +397,45 @@ class PreviewTanstackStartBuild implements BuildStrategy { } async execute(signal?: AbortSignal): Promise { - const settings = this.#buildSettings ?? await resolvePreviewBuildSettings({ - appPath: this.#appPath, - buildType: "tanstack-start", + const settings = + this.#buildSettings ?? + (await resolvePreviewBuildSettings({ + appPath: this.#appPath, + buildType: "tanstack-start", + signal, + })); + await runResolvedBuildCommand( + this.#appPath, + settings, + "TanStack Start", signal, - }); - await runResolvedBuildCommand(this.#appPath, settings, "TanStack Start", signal); + ); const outputDir = path.join(this.#appPath, settings.outputDirectory); const entrypoint = "server/index.mjs"; const entryPath = path.join(outputDir, entrypoint); - const entryStat = await unsupportedFilesystemBoundary(signal, () => stat(entryPath).catch(() => null)); + const entryStat = await unsupportedFilesystemBoundary(signal, () => + stat(entryPath).catch(() => null), + ); if (!entryStat?.isFile()) { throw new Error( - `TanStack Start build did not produce a Nitro node server entrypoint at ${joinPosix(settings.outputDirectory, entrypoint)}. ` - + `Ensure your vite.config includes the tanstackStart() and nitro() plugins with the default node preset, or update ${PRISMA_APP_CONFIG_FILENAME}.`, + `TanStack Start build did not produce a Nitro node server entrypoint at ${joinPosix(settings.outputDirectory, entrypoint)}. ` + + `Ensure your vite.config includes the tanstackStart() and nitro() plugins with the default node preset, or update ${PRISMA_APP_CONFIG_FILENAME}.`, ); } - const outDir = await unsupportedFilesystemBoundary(signal, () => mkdtemp(path.join(os.tmpdir(), "compute-build-"))); + const outDir = await unsupportedFilesystemBoundary(signal, () => + mkdtemp(path.join(os.tmpdir(), "compute-build-")), + ); try { const artifactDir = path.join(outDir, "app"); - await unsupportedFilesystemBoundary(signal, () => cp(outputDir, artifactDir, { - recursive: true, - verbatimSymlinks: true, - })); + await unsupportedFilesystemBoundary(signal, () => + cp(outputDir, artifactDir, { + recursive: true, + verbatimSymlinks: true, + }), + ); return { directory: artifactDir, @@ -409,11 +470,13 @@ class PreviewBunBuild implements BuildStrategy { } async execute(signal?: AbortSignal): Promise { - const settings = this.#buildSettings ?? await resolvePreviewBuildSettings({ - appPath: this.#appPath, - buildType: "bun", - signal, - }); + const settings = + this.#buildSettings ?? + (await resolvePreviewBuildSettings({ + appPath: this.#appPath, + buildType: "bun", + signal, + })); await runResolvedBuildCommand(this.#appPath, settings, "Bun", signal); return this.#strategy.execute(signal); @@ -450,7 +513,10 @@ export async function stageNextjsStandaloneArtifact(options: { sourceRoot, signal: options.signal, }); - await hoistPnpmDependencies(path.join(artifactRoot, "node_modules"), options.signal); + await hoistPnpmDependencies( + path.join(artifactRoot, "node_modules"), + options.signal, + ); } async function copyNextjsStaticAssets(options: { @@ -467,41 +533,58 @@ async function copyNextjsStaticAssets(options: { const publicDir = path.join(options.appPath, "public"); if (await directoryExists(publicDir, options.signal)) { - await unsupportedFilesystemBoundary(options.signal, () => cp(publicDir, path.join(serverDir, "public"), { - recursive: true, - verbatimSymlinks: true, - })); + await unsupportedFilesystemBoundary(options.signal, () => + cp(publicDir, path.join(serverDir, "public"), { + recursive: true, + verbatimSymlinks: true, + }), + ); } const staticDir = path.join(options.appPath, options.outputRoot, "static"); if (await directoryExists(staticDir, options.signal)) { - await unsupportedFilesystemBoundary(options.signal, () => cp(staticDir, path.join(serverDir, options.outputRoot, "static"), { - recursive: true, - verbatimSymlinks: true, - })); + await unsupportedFilesystemBoundary(options.signal, () => + cp(staticDir, path.join(serverDir, options.outputRoot, "static"), { + recursive: true, + verbatimSymlinks: true, + }), + ); } } -async function findNextStandaloneEntrypoint(artifactDir: string, signal?: AbortSignal): Promise { +async function findNextStandaloneEntrypoint( + artifactDir: string, + signal?: AbortSignal, +): Promise { const rootEntrypoint = path.join(artifactDir, "server.js"); - const rootStat = await unsupportedFilesystemBoundary(signal, () => stat(rootEntrypoint).catch(() => null)); + const rootStat = await unsupportedFilesystemBoundary(signal, () => + stat(rootEntrypoint).catch(() => null), + ); if (rootStat?.isFile()) { return "server.js"; } const candidates: string[] = []; await walk(artifactDir); - candidates.sort((left, right) => left.split("/").length - right.split("/").length || left.localeCompare(right)); + candidates.sort( + (left, right) => + left.split("/").length - right.split("/").length || + left.localeCompare(right), + ); const selected = candidates[0]; if (!selected) { - throw new Error(`Next.js standalone output did not contain server.js in ${artifactDir}`); + throw new Error( + `Next.js standalone output did not contain server.js in ${artifactDir}`, + ); } return selected; async function walk(directory: string): Promise { - const entries = await unsupportedFilesystemBoundary(signal, () => readdir(directory, { withFileTypes: true })); + const entries = await unsupportedFilesystemBoundary(signal, () => + readdir(directory, { withFileTypes: true }), + ); for (const entry of entries) { if (entry.name === "node_modules") { continue; @@ -514,17 +597,25 @@ async function findNextStandaloneEntrypoint(artifactDir: string, signal?: AbortS } if (entry.isFile() && entry.name === "server.js") { - candidates.push(path.relative(artifactDir, fullPath).split(path.sep).join("/")); + candidates.push( + path.relative(artifactDir, fullPath).split(path.sep).join("/"), + ); } } } } -export async function restageNextjsArtifact(artifact: BuildArtifact, appPath: string, signal?: AbortSignal): Promise { +export async function restageNextjsArtifact( + artifact: BuildArtifact, + appPath: string, + signal?: AbortSignal, +): Promise { const artifactDir = artifact.directory; const standaloneDir = path.join(appPath, ".next", "standalone"); - await unsupportedFilesystemBoundary(signal, () => rm(artifactDir, { recursive: true, force: true })); + await unsupportedFilesystemBoundary(signal, () => + rm(artifactDir, { recursive: true, force: true }), + ); await stageNextjsStandaloneArtifact({ standaloneDir, artifactDir, @@ -543,18 +634,22 @@ export async function restageNextjsArtifact(artifact: BuildArtifact, appPath: st const publicDir = path.join(appPath, "public"); if (await directoryExists(publicDir, signal)) { - await unsupportedFilesystemBoundary(signal, () => cp(publicDir, path.join(serverDir, "public"), { - recursive: true, - verbatimSymlinks: true, - })); + await unsupportedFilesystemBoundary(signal, () => + cp(publicDir, path.join(serverDir, "public"), { + recursive: true, + verbatimSymlinks: true, + }), + ); } const staticDir = path.join(appPath, ".next", "static"); if (await directoryExists(staticDir, signal)) { - await unsupportedFilesystemBoundary(signal, () => cp(staticDir, path.join(serverDir, ".next", "static"), { - recursive: true, - verbatimSymlinks: true, - })); + await unsupportedFilesystemBoundary(signal, () => + cp(staticDir, path.join(serverDir, ".next", "static"), { + recursive: true, + verbatimSymlinks: true, + }), + ); } } @@ -564,25 +659,38 @@ function nextjsServerSubpath(entrypoint: string): string { return dir === "." ? "" : dir; } -async function hoistPnpmDependencies(nodeModulesDir: string, signal?: AbortSignal): Promise { +async function hoistPnpmDependencies( + nodeModulesDir: string, + signal?: AbortSignal, +): Promise { const pnpmNodeModulesDir = path.join(nodeModulesDir, ".pnpm", "node_modules"); - if (!await directoryExists(pnpmNodeModulesDir, signal)) { + if (!(await directoryExists(pnpmNodeModulesDir, signal))) { return; } - const entries = await unsupportedFilesystemBoundary(signal, () => readdir(pnpmNodeModulesDir, { withFileTypes: true })); + const entries = await unsupportedFilesystemBoundary(signal, () => + readdir(pnpmNodeModulesDir, { withFileTypes: true }), + ); for (const entry of entries) { const sourcePath = path.join(pnpmNodeModulesDir, entry.name); if (entry.name.startsWith("@") && entry.isDirectory()) { - const scopedEntries = await unsupportedFilesystemBoundary(signal, () => readdir(sourcePath, { withFileTypes: true })); + const scopedEntries = await unsupportedFilesystemBoundary(signal, () => + readdir(sourcePath, { withFileTypes: true }), + ); for (const scopedEntry of scopedEntries) { - const scopedDestination = path.join(nodeModulesDir, entry.name, scopedEntry.name); + const scopedDestination = path.join( + nodeModulesDir, + entry.name, + scopedEntry.name, + ); if (await pathExists(scopedDestination, signal)) { continue; } - await unsupportedFilesystemBoundary(signal, () => mkdir(path.dirname(scopedDestination), { recursive: true })); + await unsupportedFilesystemBoundary(signal, () => + mkdir(path.dirname(scopedDestination), { recursive: true }), + ); await copyPathMaterializingSymlinks( path.join(sourcePath, scopedEntry.name), scopedDestination, @@ -622,7 +730,9 @@ export async function normalizeArtifactSymlinks( await walkDirectory(normalizedArtifactDir); async function walkDirectory(directory: string): Promise { - const entries = await unsupportedFilesystemBoundary(signal, () => readdir(directory, { withFileTypes: true })); + const entries = await unsupportedFilesystemBoundary(signal, () => + readdir(directory, { withFileTypes: true }), + ); for (const entry of entries) { const fullPath = path.join(directory, entry.name); @@ -636,7 +746,9 @@ export async function normalizeArtifactSymlinks( continue; } - const target = await unsupportedFilesystemBoundary(signal, () => readlink(fullPath)); + const target = await unsupportedFilesystemBoundary(signal, () => + readlink(fullPath), + ); const resolvedTarget = path.resolve(path.dirname(fullPath), target); if (isPathWithin(normalizedArtifactDir, resolvedTarget)) { @@ -644,15 +756,23 @@ export async function normalizeArtifactSymlinks( } if (!isPathWithin(normalizedAppPath, resolvedTarget)) { - throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`); + throw new Error( + `Build artifact symlink escapes the app directory: ${resolvedTarget}`, + ); } - const targetStat = await unsupportedFilesystemBoundary(signal, () => stat(resolvedTarget)); - await unsupportedFilesystemBoundary(signal, () => rm(fullPath, { force: true, recursive: true })); - await unsupportedFilesystemBoundary(signal, () => cp(resolvedTarget, fullPath, { - recursive: targetStat.isDirectory(), - dereference: true, - })); + const targetStat = await unsupportedFilesystemBoundary(signal, () => + stat(resolvedTarget), + ); + await unsupportedFilesystemBoundary(signal, () => + rm(fullPath, { force: true, recursive: true }), + ); + await unsupportedFilesystemBoundary(signal, () => + cp(resolvedTarget, fullPath, { + recursive: targetStat.isDirectory(), + dereference: true, + }), + ); if (targetStat.isDirectory()) { await walkDirectory(fullPath); @@ -672,7 +792,10 @@ function isPathWithin(rootPath: string, candidatePath: string): boolean { ); } -function isPathWithinWorkspaceDependency(sourceRoot: string, candidatePath: string): boolean { +function isPathWithinWorkspaceDependency( + sourceRoot: string, + candidatePath: string, +): boolean { return isPathWithin(path.join(sourceRoot, "node_modules"), candidatePath); } @@ -686,21 +809,31 @@ async function copyPathMaterializingSymlinks( signal?: AbortSignal; }, ): Promise { - const sourceStat = await unsupportedFilesystemBoundary(options.signal, () => lstat(sourcePath)); + const sourceStat = await unsupportedFilesystemBoundary(options.signal, () => + lstat(sourcePath), + ); if (sourceStat.isSymbolicLink()) { const resolvedTarget = await resolveSymlinkTarget(sourcePath, options); if (resolvedTarget === null) { return; } - await copyPathMaterializingSymlinks(resolvedTarget, destinationPath, options); + await copyPathMaterializingSymlinks( + resolvedTarget, + destinationPath, + options, + ); return; } if (sourceStat.isDirectory()) { - await unsupportedFilesystemBoundary(options.signal, () => mkdir(destinationPath, { recursive: true })); + await unsupportedFilesystemBoundary(options.signal, () => + mkdir(destinationPath, { recursive: true }), + ); - const entries = await unsupportedFilesystemBoundary(options.signal, () => readdir(sourcePath, { withFileTypes: true })); + const entries = await unsupportedFilesystemBoundary(options.signal, () => + readdir(sourcePath, { withFileTypes: true }), + ); for (const entry of entries) { await copyPathMaterializingSymlinks( path.join(sourcePath, entry.name), @@ -713,9 +846,15 @@ async function copyPathMaterializingSymlinks( } if (sourceStat.isFile()) { - await unsupportedFilesystemBoundary(options.signal, () => mkdir(path.dirname(destinationPath), { recursive: true })); - await unsupportedFilesystemBoundary(options.signal, () => copyFile(sourcePath, destinationPath)); - await unsupportedFilesystemBoundary(options.signal, () => chmod(destinationPath, sourceStat.mode)); + await unsupportedFilesystemBoundary(options.signal, () => + mkdir(path.dirname(destinationPath), { recursive: true }), + ); + await unsupportedFilesystemBoundary(options.signal, () => + copyFile(sourcePath, destinationPath), + ); + await unsupportedFilesystemBoundary(options.signal, () => + chmod(destinationPath, sourceStat.mode), + ); } } @@ -728,7 +867,9 @@ async function resolveSymlinkTarget( signal?: AbortSignal; }, ): Promise { - const linkTarget = await unsupportedFilesystemBoundary(options.signal, () => readlink(symlinkPath)); + const linkTarget = await unsupportedFilesystemBoundary(options.signal, () => + readlink(symlinkPath), + ); const resolvedTarget = path.resolve(path.dirname(symlinkPath), linkTarget); if (await pathExists(resolvedTarget, options.signal)) { @@ -736,7 +877,9 @@ async function resolveSymlinkTarget( !isPathWithin(options.appRoot, resolvedTarget) && !isPathWithinWorkspaceDependency(options.sourceRoot, resolvedTarget) ) { - throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`); + throw new Error( + `Build artifact symlink escapes the app directory: ${resolvedTarget}`, + ); } return resolvedTarget; @@ -776,7 +919,10 @@ function isPnpmHoistLink(symlinkPath: string): boolean { return false; } -async function pathExists(targetPath: string, signal?: AbortSignal): Promise { +async function pathExists( + targetPath: string, + signal?: AbortSignal, +): Promise { try { await unsupportedFilesystemBoundary(signal, () => stat(targetPath)); return true; @@ -786,9 +932,14 @@ async function pathExists(targetPath: string, signal?: AbortSignal): Promise { +async function directoryExists( + targetPath: string, + signal?: AbortSignal, +): Promise { try { - const targetStat = await unsupportedFilesystemBoundary(signal, () => stat(targetPath)); + const targetStat = await unsupportedFilesystemBoundary(signal, () => + stat(targetPath), + ); return targetStat.isDirectory(); } catch (error) { if (signal?.aborted) throw error; @@ -796,16 +947,19 @@ async function directoryExists(targetPath: string, signal?: AbortSignal): Promis } } -async function resolveSourceRoot(appRoot: string, signal?: AbortSignal): Promise { +async function resolveSourceRoot( + appRoot: string, + signal?: AbortSignal, +): Promise { let current = path.resolve(appRoot); while (true) { if ( - await pathExists(path.join(current, ".git"), signal) || - await pathExists(path.join(current, "pnpm-workspace.yaml"), signal) || - await pathExists(path.join(current, "bun.lock"), signal) || - await pathExists(path.join(current, "bun.lockb"), signal) || - await packageJsonDeclaresWorkspaces(current, signal) + (await pathExists(path.join(current, ".git"), signal)) || + (await pathExists(path.join(current, "pnpm-workspace.yaml"), signal)) || + (await pathExists(path.join(current, "bun.lock"), signal)) || + (await pathExists(path.join(current, "bun.lockb"), signal)) || + (await packageJsonDeclaresWorkspaces(current, signal)) ) { return current; } @@ -819,10 +973,16 @@ async function resolveSourceRoot(appRoot: string, signal?: AbortSignal): Promise } } -async function packageJsonDeclaresWorkspaces(directory: string, signal?: AbortSignal): Promise { +async function packageJsonDeclaresWorkspaces( + directory: string, + signal?: AbortSignal, +): Promise { signal?.throwIfAborted(); try { - const content = await readFile(path.join(directory, "package.json"), { encoding: "utf8", signal }); + const content = await readFile(path.join(directory, "package.json"), { + encoding: "utf8", + signal, + }); const parsed = JSON.parse(content) as { workspaces?: unknown }; return Boolean(parsed.workspaces); } catch (error) { @@ -831,7 +991,10 @@ async function packageJsonDeclaresWorkspaces(directory: string, signal?: AbortSi } } -async function unsupportedFilesystemBoundary(signal: AbortSignal | undefined, operation: () => Promise): Promise { +async function unsupportedFilesystemBoundary( + signal: AbortSignal | undefined, + operation: () => Promise, +): Promise { // These Node fs promise APIs do not accept AbortSignal; check immediately before and after the boundary. signal?.throwIfAborted(); const result = await operation(); diff --git a/packages/cli/src/lib/app/preview-interaction.ts b/packages/cli/src/lib/app/preview-interaction.ts index 27ec2c6..5e52cd0 100644 --- a/packages/cli/src/lib/app/preview-interaction.ts +++ b/packages/cli/src/lib/app/preview-interaction.ts @@ -1,4 +1,8 @@ -import type { DeployInteraction, RegionInfo, ServiceInfo } from "@prisma/compute-sdk"; +import type { + DeployInteraction, + RegionInfo, + ServiceInfo, +} from "@prisma/compute-sdk"; import { selectPrompt, textPrompt } from "../../shell/prompt"; import type { CommandContext } from "../../shell/runtime"; @@ -6,12 +10,18 @@ import type { CommandContext } from "../../shell/runtime"; const CREATE_NEW_APP = "__create_new_app__"; export const PREVIEW_DEFAULT_REGION = "eu-central-1"; -export function createPreviewDeployInteraction(context: CommandContext): DeployInteraction { +export function createPreviewDeployInteraction( + context: CommandContext, +): DeployInteraction { return { async selectService(services: ServiceInfo[]): Promise { const sorted = services .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), + ); const selection = await selectPrompt({ input: context.runtime.stdin, @@ -36,7 +46,8 @@ export function createPreviewDeployInteraction(context: CommandContext): DeployI input: context.runtime.stdin, output: context.runtime.stderr, message: "App name", - validate: (value) => (!value?.trim() ? "App name is required" : undefined), + validate: (value) => + !value?.trim() ? "App name is required" : undefined, }).then((value) => value.trim()); }, async selectRegion(_regions: RegionInfo[]): Promise { diff --git a/packages/cli/src/lib/app/preview-progress.ts b/packages/cli/src/lib/app/preview-progress.ts index a308689..eeef44c 100644 --- a/packages/cli/src/lib/app/preview-progress.ts +++ b/packages/cli/src/lib/app/preview-progress.ts @@ -1,4 +1,8 @@ -import type { DeployProgress, PromoteProgress, UpdateEnvProgress } from "@prisma/compute-sdk"; +import type { + DeployProgress, + PromoteProgress, + UpdateEnvProgress, +} from "@prisma/compute-sdk"; import type { Writable } from "node:stream"; import { renderDeployOutputRows } from "./deploy-output"; diff --git a/packages/cli/src/lib/app/preview-provider.ts b/packages/cli/src/lib/app/preview-provider.ts index 7b75d4f..94b1c78 100644 --- a/packages/cli/src/lib/app/preview-provider.ts +++ b/packages/cli/src/lib/app/preview-provider.ts @@ -1,6 +1,11 @@ import path from "node:path"; -import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk"; +import { + ApiError, + CancelledError, + ComputeClient, + streamLogs, +} from "@prisma/compute-sdk"; import type { PortMapping, StreamRecord } from "@prisma/compute-sdk"; import type { ManagementApiClient } from "@prisma/management-api-sdk"; @@ -39,7 +44,10 @@ export interface PreviewBranchRecord { role: BranchKind; } -export type { PreviewBranchDatabaseRecord, PreviewEnvironmentVariableRecord } from "./preview-branch-database"; +export type { + PreviewBranchDatabaseRecord, + PreviewEnvironmentVariableRecord, +} from "./preview-branch-database"; export interface PreviewDeploymentRecord { id: string; @@ -113,8 +121,16 @@ export class PreviewDomainApiError extends Error { readonly code: string | null; readonly hint: string | null; - constructor(options: { summary: string; status: number; message: string; code?: string | null; hint?: string | null }) { - super(`${options.summary}: ${options.message}${options.hint ? ` ${options.hint}` : ""}`); + constructor(options: { + summary: string; + status: number; + message: string; + code?: string | null; + hint?: string | null; + }) { + super( + `${options.summary}: ${options.message}${options.hint ? ` ${options.hint}` : ""}`, + ); this.name = "PreviewDomainApiError"; this.status = options.status; this.code = options.code ?? null; @@ -123,15 +139,24 @@ export class PreviewDomainApiError extends Error { } export interface PreviewAppProvider { - createProject(options: { name: string; signal?: AbortSignal }): Promise; - resolveBranch(projectId: string, options: { branchName: string; signal?: AbortSignal }): Promise; + createProject(options: { + name: string; + signal?: AbortSignal; + }): Promise; + resolveBranch( + projectId: string, + options: { branchName: string; signal?: AbortSignal }, + ): Promise; createBranchDatabase(options: { projectId: string; branchId: string; branchName: string; signal?: AbortSignal; }): Promise; - deleteBranchDatabase(options: { databaseId: string; signal?: AbortSignal }): Promise; + deleteBranchDatabase(options: { + databaseId: string; + signal?: AbortSignal; + }): Promise; listEnvironmentVariables(options: { projectId: string; className?: "production" | "preview"; @@ -147,15 +172,44 @@ export interface PreviewAppProvider { value: string; signal?: AbortSignal; }): Promise; - updateEnvironmentVariable(options: { envVarId: string; value: string; signal?: AbortSignal }): Promise; - deleteEnvironmentVariable(options: { envVarId: string; signal?: AbortSignal }): Promise; - listApps(projectId: string, options?: { branchName?: string; signal?: AbortSignal }): Promise; - removeApp(appId: string, options?: { signal?: AbortSignal }): Promise; - listDomains(appId: string, options?: { signal?: AbortSignal }): Promise; - addDomain(options: { appId: string; hostname: string; signal?: AbortSignal }): Promise<{ domain: PreviewDomainRecord; existing: boolean }>; - showDomain(domainId: string, options?: { signal?: AbortSignal }): Promise; - removeDomain(domainId: string, options?: { signal?: AbortSignal }): Promise; - retryDomain(domainId: string, options?: { signal?: AbortSignal }): Promise; + updateEnvironmentVariable(options: { + envVarId: string; + value: string; + signal?: AbortSignal; + }): Promise; + deleteEnvironmentVariable(options: { + envVarId: string; + signal?: AbortSignal; + }): Promise; + listApps( + projectId: string, + options?: { branchName?: string; signal?: AbortSignal }, + ): Promise; + removeApp( + appId: string, + options?: { signal?: AbortSignal }, + ): Promise; + listDomains( + appId: string, + options?: { signal?: AbortSignal }, + ): Promise; + addDomain(options: { + appId: string; + hostname: string; + signal?: AbortSignal; + }): Promise<{ domain: PreviewDomainRecord; existing: boolean }>; + showDomain( + domainId: string, + options?: { signal?: AbortSignal }, + ): Promise; + removeDomain( + domainId: string, + options?: { signal?: AbortSignal }, + ): Promise; + retryDomain( + domainId: string, + options?: { signal?: AbortSignal }, + ): Promise; promoteDeployment(options: { appId: string; deploymentId: string; @@ -190,11 +244,17 @@ export interface PreviewAppProvider { deploymentId: string; signal?: AbortSignal; }): Promise; - listDeployments(appId: string, options?: { signal?: AbortSignal }): Promise<{ + listDeployments( + appId: string, + options?: { signal?: AbortSignal }, + ): Promise<{ app: PreviewAppRecord; deployments: PreviewDeploymentRecord[]; }>; - showDeployment(deploymentId: string, options?: { signal?: AbortSignal }): Promise; + showDeployment( + deploymentId: string, + options?: { signal?: AbortSignal }, + ): Promise; streamDeploymentLogs(options: { deploymentId: string; signal?: AbortSignal; @@ -213,7 +273,10 @@ export function createPreviewAppProvider( return { async createProject(options) { - const projectResult = await sdk.createProject({ name: options.name, signal: options.signal }); + const projectResult = await sdk.createProject({ + name: options.name, + signal: options.signal, + }); if (projectResult.isErr()) { throw new Error(projectResult.error.message); } @@ -271,7 +334,10 @@ export function createPreviewAppProvider( }, async removeApp(appId, options) { - const appResult = await sdk.showService({ serviceId: appId, signal: options?.signal }); + const appResult = await sdk.showService({ + serviceId: appId, + signal: options?.signal, + }); if (appResult.isErr()) { throw new Error(appResult.error.message); } @@ -299,20 +365,28 @@ export function createPreviewAppProvider( }, async addDomain(options) { - const result = await client.POST("/v1/compute-services/{computeServiceId}/domains", { - params: { - path: { computeServiceId: options.appId }, - }, - body: { - hostname: options.hostname, + const result = await client.POST( + "/v1/compute-services/{computeServiceId}/domains", + { + params: { + path: { computeServiceId: options.appId }, + }, + body: { + hostname: options.hostname, + }, + signal: options.signal, }, - signal: options.signal, - }); + ); if (result.error || !result.data) { if (result.response.status === 409) { - const existing = (await listComputeServiceDomains(client, options.appId, options.signal)) - .find((domain) => sameHostname(domain.hostname, options.hostname)); + const existing = ( + await listComputeServiceDomains( + client, + options.appId, + options.signal, + ) + ).find((domain) => sameHostname(domain.hostname, options.hostname)); if (existing) { return { domain: existing, @@ -321,7 +395,11 @@ export function createPreviewAppProvider( } } - throw domainApiCallError("Failed to add custom domain", result.response, result.error); + throw domainApiCallError( + "Failed to add custom domain", + result.response, + result.error, + ); } return { @@ -339,7 +417,11 @@ export function createPreviewAppProvider( }); if (result.error || !result.data) { - throw domainApiCallError("Failed to show custom domain", result.response, result.error); + throw domainApiCallError( + "Failed to show custom domain", + result.response, + result.error, + ); } return normalizeDomainRecord(result.data.data); @@ -354,7 +436,11 @@ export function createPreviewAppProvider( }); if (result.error) { - throw domainApiCallError("Failed to remove custom domain", result.response, result.error); + throw domainApiCallError( + "Failed to remove custom domain", + result.response, + result.error, + ); } }, @@ -367,7 +453,11 @@ export function createPreviewAppProvider( }); if (result.error || !result.data) { - throw domainApiCallError("Failed to retry custom domain", result.response, result.error); + throw domainApiCallError( + "Failed to retry custom domain", + result.response, + result.error, + ); } return normalizeDomainRecord(result.data.data); @@ -448,7 +538,11 @@ export function createPreviewAppProvider( deployment: { id: deployed.versionId, status: "running", - url: toAbsoluteUrl(deployed.serviceEndpointDomain ?? deployed.versionEndpointDomain ?? null), + url: toAbsoluteUrl( + deployed.serviceEndpointDomain ?? + deployed.versionEndpointDomain ?? + null, + ), }, }; }, @@ -482,7 +576,10 @@ export function createPreviewAppProvider( const [serviceResult, versionResult] = await Promise.all([ sdk.showService({ serviceId: options.appId, signal: options.signal }), - sdk.showVersion({ versionId: updateResult.value.versionId, signal: options.signal }), + sdk.showVersion({ + versionId: updateResult.value.versionId, + signal: options.signal, + }), ]); if (serviceResult.isErr()) { @@ -500,13 +597,19 @@ export function createPreviewAppProvider( name: serviceResult.value.name, region: serviceResult.value.region ?? null, liveDeploymentId: serviceResult.value.latestVersionId ?? null, - liveUrl: toAbsoluteUrl(serviceResult.value.serviceEndpointDomain ?? null), + liveUrl: toAbsoluteUrl( + serviceResult.value.serviceEndpointDomain ?? null, + ), }, deployment: { id: versionResult.value.id, status: versionResult.value.status, createdAt: versionResult.value.createdAt, - url: toAbsoluteUrl(serviceResult.value.serviceEndpointDomain ?? versionResult.value.previewDomain ?? null), + url: toAbsoluteUrl( + serviceResult.value.serviceEndpointDomain ?? + versionResult.value.previewDomain ?? + null, + ), live: true, }, variables: envVarNames(versionResult.value.envVars), @@ -516,7 +619,10 @@ export function createPreviewAppProvider( async listAppEnvNames(options) { const [serviceResult, versionResult] = await Promise.all([ sdk.showService({ serviceId: options.appId, signal: options.signal }), - sdk.showVersion({ versionId: options.deploymentId, signal: options.signal }), + sdk.showVersion({ + versionId: options.deploymentId, + signal: options.signal, + }), ]); if (serviceResult.isErr()) { @@ -534,7 +640,9 @@ export function createPreviewAppProvider( name: serviceResult.value.name, region: serviceResult.value.region ?? null, liveDeploymentId: serviceResult.value.latestVersionId ?? null, - liveUrl: toAbsoluteUrl(serviceResult.value.serviceEndpointDomain ?? null), + liveUrl: toAbsoluteUrl( + serviceResult.value.serviceEndpointDomain ?? null, + ), }, deployment: { id: versionResult.value.id, @@ -586,16 +694,26 @@ export function createPreviewAppProvider( }, async showDeployment(deploymentId, options) { - const deploymentResult = await sdk.showVersion({ versionId: deploymentId, signal: options?.signal }); + const deploymentResult = await sdk.showVersion({ + versionId: deploymentId, + signal: options?.signal, + }); if (deploymentResult.isErr()) { - if (ApiError.is(deploymentResult.error) && deploymentResult.error.statusCode === 404) { + if ( + ApiError.is(deploymentResult.error) && + deploymentResult.error.statusCode === 404 + ) { return null; } throw new Error(deploymentResult.error.message); } - const app = await findAppForDeployment(sdk, deploymentId, options?.signal); + const app = await findAppForDeployment( + sdk, + deploymentId, + options?.signal, + ); return { app, @@ -611,7 +729,9 @@ export function createPreviewAppProvider( async streamDeploymentLogs(streamOptions) { if (!options?.baseUrl || !options.getToken) { - throw new Error("Log streaming requires an authenticated API base URL and token."); + throw new Error( + "Log streaming requires an authenticated API base URL and token.", + ); } const result = await streamLogs( @@ -703,7 +823,11 @@ async function listBranches( signal: options.signal, }); if (result.error || !result.data) { - throw apiCallError("Failed to list branches", result.response, result.error); + throw apiCallError( + "Failed to list branches", + result.response, + result.error, + ); } return result.data.data.map((branch) => ({ @@ -744,7 +868,11 @@ async function resolveOrCreateBranch( } } - throw apiCallError(`Failed to create branch "${options.gitName}"`, result.response, result.error); + throw apiCallError( + `Failed to create branch "${options.gitName}"`, + result.response, + result.error, + ); } const branch = result.data.data; @@ -783,7 +911,7 @@ async function listComputeServices( throw apiCallError("Failed to list apps", result.response, result.error); } - services.push(...result.data.data as RawComputeServiceRecord[]); + services.push(...(result.data.data as RawComputeServiceRecord[])); if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) { break; @@ -806,15 +934,22 @@ async function listComputeServiceDomains( computeServiceId: string, signal?: AbortSignal, ): Promise { - const result = await client.GET("/v1/compute-services/{computeServiceId}/domains", { - params: { - path: { computeServiceId }, + const result = await client.GET( + "/v1/compute-services/{computeServiceId}/domains", + { + params: { + path: { computeServiceId }, + }, + signal, }, - signal, - }); + ); if (result.error || !result.data) { - throw domainApiCallError("Failed to list custom domains", result.response, result.error); + throw domainApiCallError( + "Failed to list custom domains", + result.response, + result.error, + ); } return result.data.data.map((domain) => normalizeDomainRecord(domain)); @@ -838,14 +973,20 @@ function normalizeDomainRecord(domain: RawDomainRecord): PreviewDomainRecord { }; } -function normalizeDomainDnsRecords(records: RawDomainDnsRecord[] | null | undefined): PreviewDomainDnsRecord[] { +function normalizeDomainDnsRecords( + records: RawDomainDnsRecord[] | null | undefined, +): PreviewDomainDnsRecord[] { if (!Array.isArray(records)) { return []; } return records .map((record) => { - if (typeof record.type !== "string" || typeof record.name !== "string" || typeof record.value !== "string") { + if ( + typeof record.type !== "string" || + typeof record.name !== "string" || + typeof record.value !== "string" + ) { return null; } @@ -860,7 +1001,10 @@ function normalizeDomainDnsRecords(records: RawDomainDnsRecord[] | null | undefi } function sameHostname(left: string, right: string): boolean { - return normalizeHostnameForComparison(left) === normalizeHostnameForComparison(right); + return ( + normalizeHostnameForComparison(left) === + normalizeHostnameForComparison(right) + ); } function normalizeHostnameForComparison(hostname: string): string { @@ -908,7 +1052,11 @@ async function createBranchApp( } } - throw apiCallError(`Failed to create app "${options.appName}"`, result.response, result.error); + throw apiCallError( + `Failed to create app "${options.appName}"`, + result.response, + result.error, + ); } const service = result.data.data as RawComputeServiceRecord; @@ -928,7 +1076,8 @@ function apiCallError( return new Error("Resource Not Found"); } - const message = error.error?.message ?? `Management API returned HTTP ${response.status}.`; + const message = + error.error?.message ?? `Management API returned HTTP ${response.status}.`; const hint = error.error?.hint ? ` ${error.error.hint}` : ""; return new Error(`${summary}: ${message}${hint}`); } @@ -942,7 +1091,9 @@ function domainApiCallError( summary, status: response.status, code: error.error?.code ?? null, - message: error.error?.message ?? `Management API returned HTTP ${response.status}.`, + message: + error.error?.message ?? + `Management API returned HTTP ${response.status}.`, hint: error.error?.hint ?? null, }); } @@ -958,13 +1109,19 @@ async function findAppForDeployment( } for (const project of projectsResult.value) { - const servicesResult = await sdk.listServices({ projectId: project.id, signal }); + const servicesResult = await sdk.listServices({ + projectId: project.id, + signal, + }); if (servicesResult.isErr()) { throw new Error(servicesResult.error.message); } for (const service of servicesResult.value) { - const detailResult = await sdk.showService({ serviceId: service.id, signal }); + const detailResult = await sdk.showService({ + serviceId: service.id, + signal, + }); if (detailResult.isErr()) { throw new Error(detailResult.error.message); } @@ -974,14 +1131,19 @@ async function findAppForDeployment( name: detailResult.value.name, region: detailResult.value.region ?? null, liveDeploymentId: detailResult.value.latestVersionId ?? null, - liveUrl: toAbsoluteUrl(detailResult.value.serviceEndpointDomain ?? null), + liveUrl: toAbsoluteUrl( + detailResult.value.serviceEndpointDomain ?? null, + ), }; if (app.liveDeploymentId === deploymentId) { return app; } - const versionsResult = await sdk.listVersions({ serviceId: service.id, signal }); + const versionsResult = await sdk.listVersions({ + serviceId: service.id, + signal, + }); if (versionsResult.isErr()) { throw new Error(versionsResult.error.message); } @@ -1000,5 +1162,7 @@ function toAbsoluteUrl(url: string | null): string | null { return null; } - return url.startsWith("https://") || url.startsWith("http://") ? url : `https://${url}`; + return url.startsWith("https://") || url.startsWith("http://") + ? url + : `https://${url}`; } diff --git a/packages/cli/src/lib/app/production-deploy-gate.ts b/packages/cli/src/lib/app/production-deploy-gate.ts index abc98f4..6bcc703 100644 --- a/packages/cli/src/lib/app/production-deploy-gate.ts +++ b/packages/cli/src/lib/app/production-deploy-gate.ts @@ -2,7 +2,10 @@ import { CliError } from "../../shell/errors"; import { confirmPrompt } from "../../shell/prompt"; import { canPrompt, type CommandContext } from "../../shell/runtime"; import type { BranchKind } from "../../types/branch"; -import type { PreviewAppProvider, PreviewDeploymentRecord } from "./preview-provider"; +import type { + PreviewAppProvider, + PreviewDeploymentRecord, +} from "./preview-provider"; export async function enforceProductionDeployGate( context: CommandContext, @@ -23,10 +26,13 @@ export async function enforceProductionDeployGate( return { firstProductionDeploy: true }; } - const deploymentsResult = await provider.listDeployments(options.appId).catch((error) => { - throw productionDeployInspectionFailedError(error); - }); - const currentLiveDeployment = resolveCurrentProductionDeployment(deploymentsResult); + const deploymentsResult = await provider + .listDeployments(options.appId) + .catch((error) => { + throw productionDeployInspectionFailedError(error); + }); + const currentLiveDeployment = + resolveCurrentProductionDeployment(deploymentsResult); if (!currentLiveDeployment) { renderFirstProductionDeployLine(context, options.appName); return { firstProductionDeploy: true }; @@ -60,27 +66,40 @@ export async function enforceProductionDeployGate( return { firstProductionDeploy: false }; } -function resolveCurrentProductionDeployment(result: Awaited>): PreviewDeploymentRecord | null { +function resolveCurrentProductionDeployment( + result: Awaited>, +): PreviewDeploymentRecord | null { if (result.deployments.length === 0) { return null; } if (result.app.liveDeploymentId) { - const live = result.deployments.find((deployment) => deployment.id === result.app.liveDeploymentId); + const live = result.deployments.find( + (deployment) => deployment.id === result.app.liveDeploymentId, + ); if (live) { return live; } } - return result.deployments.find((deployment) => deployment.live === true) ?? result.deployments[0] ?? null; + return ( + result.deployments.find((deployment) => deployment.live === true) ?? + result.deployments[0] ?? + null + ); } -function renderFirstProductionDeployLine(context: CommandContext, appName: string): void { +function renderFirstProductionDeployLine( + context: CommandContext, + appName: string, +): void { if (context.flags.json || context.flags.quiet) { return; } - context.output.stderr.write(`First deploy of "${appName}" -- promoting to production.\n\n`); + context.output.stderr.write( + `First deploy of "${appName}" -- promoting to production.\n\n`, + ); } function renderProductionDeployYesLine(context: CommandContext): void { diff --git a/packages/cli/src/lib/auth/auth-ops.ts b/packages/cli/src/lib/auth/auth-ops.ts index 759ce31..ab35327 100644 --- a/packages/cli/src/lib/auth/auth-ops.ts +++ b/packages/cli/src/lib/auth/auth-ops.ts @@ -21,7 +21,9 @@ function decodeJwtPayload(token: string): Record { function emailFromClaims(claims: Record): string | null { const email = claims.email; - return typeof email === "string" && email.trim().length > 0 ? email.trim() : null; + return typeof email === "string" && email.trim().length > 0 + ? email.trim() + : null; } function workspaceIdFromClaims(claims: Record): string | null { @@ -32,11 +34,17 @@ function workspaceIdFromClaims(claims: Record): string | null { return id.length > 0 ? id : null; } -export async function performLogin(env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise { +export async function performLogin( + env: NodeJS.ProcessEnv, + signal?: AbortSignal, +): Promise { await login({ tokenStorage: new FileTokenStorage(env, signal), env, signal }); } -export async function readAuthState(env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise { +export async function readAuthState( + env: NodeJS.ProcessEnv, + signal?: AbortSignal, +): Promise { // PRISMA_SERVICE_TOKEN is the headless / CI auth surface. When it is set, derive // auth state from the token itself and intentionally skip FileTokenStorage, // so behavior is independent of any OAuth session that happens to be stored @@ -227,6 +235,9 @@ async function readCurrentPrincipalAuthState( } } -export async function performLogout(env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise { +export async function performLogout( + env: NodeJS.ProcessEnv, + signal?: AbortSignal, +): Promise { await new FileTokenStorage(env, signal).clearTokens(); } diff --git a/packages/cli/src/lib/auth/client.ts b/packages/cli/src/lib/auth/client.ts index f5feef0..c6f6613 100644 --- a/packages/cli/src/lib/auth/client.ts +++ b/packages/cli/src/lib/auth/client.ts @@ -17,14 +17,22 @@ export function getAuthFilePath(env: NodeJS.ProcessEnv = process.env): string { } if (process.platform === "darwin") { - return path.join(os.homedir(), "Library", "Application Support", "prisma", "auth.json"); + return path.join( + os.homedir(), + "Library", + "Application Support", + "prisma", + "auth.json", + ); } if (process.platform === "win32") { - const appData = env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"); + const appData = + env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"); return path.join(appData, "prisma", "auth.json"); } - const xdgConfigHome = env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"); + const xdgConfigHome = + env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"); return path.join(xdgConfigHome, "prisma", "auth.json"); } diff --git a/packages/cli/src/lib/auth/login.ts b/packages/cli/src/lib/auth/login.ts index f4268bb..d1cd66a 100644 --- a/packages/cli/src/lib/auth/login.ts +++ b/packages/cli/src/lib/auth/login.ts @@ -134,12 +134,15 @@ export async function login(options: LoginOptions = {}): Promise { // Only race the paste flow when stdin is a TTY we can actually prompt on. // Without one (CI, pipes, tests) the browser callback is the only path. const callbackResult = interactive - ? Promise.race([httpResult, consumePastedCallback({ - input, - output, - signal: pasteAbort.signal, - complete: completeOnce, - })]) + ? Promise.race([ + httpResult, + consumePastedCallback({ + input, + output, + signal: pasteAbort.signal, + complete: completeOnce, + }), + ]) : httpResult; await Promise.all([state.openLoginPage(interactive), callbackResult]); diff --git a/packages/cli/src/lib/database/provider.ts b/packages/cli/src/lib/database/provider.ts index cef3456..90d573e 100644 --- a/packages/cli/src/lib/database/provider.ts +++ b/packages/cli/src/lib/database/provider.ts @@ -1,7 +1,10 @@ import type { ManagementApiClient } from "@prisma/management-api-sdk"; import { CliError } from "../../shell/errors"; -import type { DatabaseConnectionSummary, DatabaseSummary } from "../../types/database"; +import type { + DatabaseConnectionSummary, + DatabaseSummary, +} from "../../types/database"; export interface DatabaseCreateInput { projectId: string; @@ -34,15 +37,29 @@ export interface DatabaseProvider { branchName?: string; signal?: AbortSignal; }): Promise; - showDatabase(databaseId: string, options?: { - projectId?: string; - signal?: AbortSignal; - }): Promise; + showDatabase( + databaseId: string, + options?: { + projectId?: string; + signal?: AbortSignal; + }, + ): Promise; createDatabase(options: DatabaseCreateInput): Promise; - removeDatabase(databaseId: string, options?: { signal?: AbortSignal }): Promise; - listConnections(databaseId: string, options?: { signal?: AbortSignal }): Promise; - createConnection(options: DatabaseConnectionCreateInput): Promise; - removeConnection(connectionId: string, options?: { signal?: AbortSignal }): Promise; + removeDatabase( + databaseId: string, + options?: { signal?: AbortSignal }, + ): Promise; + listConnections( + databaseId: string, + options?: { signal?: AbortSignal }, + ): Promise; + createConnection( + options: DatabaseConnectionCreateInput, + ): Promise; + removeConnection( + connectionId: string, + options?: { signal?: AbortSignal }, + ): Promise; } interface RawApiErrorBody { @@ -97,7 +114,9 @@ interface RawDatabaseRecord { connections?: RawDatabaseConnectionRecord[] | null; } -export function createManagementDatabaseProvider(client: ManagementApiClient): DatabaseProvider { +export function createManagementDatabaseProvider( + client: ManagementApiClient, +): DatabaseProvider { return { async listDatabases(options) { const databases: RawDatabaseRecord[] = []; @@ -116,18 +135,27 @@ export function createManagementDatabaseProvider(client: ManagementApiClient): D signal: options.signal, }); if (result.error || !result.data) { - throw databaseApiError("Failed to list databases", result.response, result.error); + throw databaseApiError( + "Failed to list databases", + result.response, + result.error, + ); } - databases.push(...result.data.data as RawDatabaseRecord[]); + databases.push(...(result.data.data as RawDatabaseRecord[])); - if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) { + if ( + !result.data.pagination.hasMore || + !result.data.pagination.nextCursor + ) { break; } cursor = result.data.pagination.nextCursor; } - return databases.map((database) => normalizeDatabase(database, options.projectId)); + return databases.map((database) => + normalizeDatabase(database, options.projectId), + ); }, async showDatabase(databaseId, options) { @@ -141,11 +169,18 @@ export function createManagementDatabaseProvider(client: ManagementApiClient): D return null; } if (result.error || !result.data) { - throw databaseApiError("Failed to show database", result.response, result.error); + throw databaseApiError( + "Failed to show database", + result.response, + result.error, + ); } const database = result.data.data as RawDatabaseRecord; - return normalizeDatabase(database, requireDatabaseProjectId(database, options?.projectId)); + return normalizeDatabase( + database, + requireDatabaseProjectId(database, options?.projectId), + ); }, async createDatabase(options) { @@ -160,10 +195,17 @@ export function createManagementDatabaseProvider(client: ManagementApiClient): D signal: options.signal, }); if (result.error || !result.data) { - throw databaseApiError("Failed to create database", result.response, result.error); + throw databaseApiError( + "Failed to create database", + result.response, + result.error, + ); } - return normalizeCreatedDatabase(result.data.data as RawDatabaseRecord, options.projectId); + return normalizeCreatedDatabase( + result.data.data as RawDatabaseRecord, + options.projectId, + ); }, async removeDatabase(databaseId, options) { @@ -174,39 +216,62 @@ export function createManagementDatabaseProvider(client: ManagementApiClient): D signal: options?.signal, }); if (result.error) { - throw databaseApiError("Failed to remove database", result.response, result.error); + throw databaseApiError( + "Failed to remove database", + result.response, + result.error, + ); } }, async listConnections(databaseId, options) { - const result = await client.GET("/v1/databases/{databaseId}/connections", { - params: { - path: { databaseId }, + const result = await client.GET( + "/v1/databases/{databaseId}/connections", + { + params: { + path: { databaseId }, + }, + signal: options?.signal, }, - signal: options?.signal, - }); + ); if (result.error || !result.data) { - throw databaseApiError("Failed to list database connections", result.response, result.error); + throw databaseApiError( + "Failed to list database connections", + result.response, + result.error, + ); } - return (result.data.data as RawDatabaseConnectionRecord[]).map((connection) => normalizeConnection(connection, databaseId)); + return (result.data.data as RawDatabaseConnectionRecord[]).map( + (connection) => normalizeConnection(connection, databaseId), + ); }, async createConnection(options) { - const result = await client.POST("/v1/databases/{databaseId}/connections", { - params: { - path: { databaseId: options.databaseId }, + const result = await client.POST( + "/v1/databases/{databaseId}/connections", + { + params: { + path: { databaseId: options.databaseId }, + }, + body: { + name: options.name, + } as never, + signal: options.signal, }, - body: { - name: options.name, - } as never, - signal: options.signal, - }); + ); if (result.error || !result.data) { - throw databaseApiError("Failed to create database connection", result.response, result.error); + throw databaseApiError( + "Failed to create database connection", + result.response, + result.error, + ); } - return normalizeCreatedConnection(result.data.data as RawDatabaseConnectionRecord, options.databaseId); + return normalizeCreatedConnection( + result.data.data as RawDatabaseConnectionRecord, + options.databaseId, + ); }, async removeConnection(connectionId, options) { @@ -217,19 +282,31 @@ export function createManagementDatabaseProvider(client: ManagementApiClient): D signal: options?.signal, }); if (result.error) { - throw databaseApiError("Failed to remove database connection", result.response, result.error); + throw databaseApiError( + "Failed to remove database connection", + result.response, + result.error, + ); } }, }; } -export function normalizeDatabase(database: RawDatabaseRecord, fallbackProjectId: string): DatabaseSummary { +export function normalizeDatabase( + database: RawDatabaseRecord, + fallbackProjectId: string, +): DatabaseSummary { return { id: database.id, name: database.name, projectId: database.projectId ?? fallbackProjectId, branchId: database.branchId ?? database.branch?.id ?? null, - branchName: database.branchGitName ?? database.branchName ?? database.branch?.gitName ?? database.branch?.name ?? null, + branchName: + database.branchGitName ?? + database.branchName ?? + database.branch?.gitName ?? + database.branch?.name ?? + null, region: normalizeRegion(database), status: database.status ?? null, isDefault: database.isDefault ?? null, @@ -249,7 +326,10 @@ export function normalizeConnection( }; } -export function normalizeCreatedDatabase(database: RawDatabaseRecord, fallbackProjectId: string): DatabaseCreateRecord { +export function normalizeCreatedDatabase( + database: RawDatabaseRecord, + fallbackProjectId: string, +): DatabaseCreateRecord { const rawConnection = database.connections?.[0]; if (!rawConnection) { throw new CliError({ @@ -282,7 +362,9 @@ export function normalizeCreatedConnection( why: "Database connection strings are one-time-view secrets, but the Management API did not include one in this create response.", fix: "Create another database connection and store the returned URL immediately.", exitCode: 1, - nextSteps: [`prisma-cli database connection create ${fallbackDatabaseId}`], + nextSteps: [ + `prisma-cli database connection create ${fallbackDatabaseId}`, + ], }); } @@ -299,7 +381,10 @@ function normalizeRegion(database: RawDatabaseRecord): string | null { return database.region?.id ?? database.regionId ?? null; } -function requireDatabaseProjectId(database: RawDatabaseRecord, fallbackProjectId: string | undefined): string { +function requireDatabaseProjectId( + database: RawDatabaseRecord, + fallbackProjectId: string | undefined, +): string { const projectId = database.projectId ?? fallbackProjectId; if (projectId) { return projectId; @@ -316,12 +401,16 @@ function requireDatabaseProjectId(database: RawDatabaseRecord, fallbackProjectId }); } -function extractConnectionString(connection: RawDatabaseConnectionRecord): string | null { - return connection.endpoints?.pooled?.connectionString - ?? connection.connectionString - ?? connection.endpoints?.direct?.connectionString - ?? connection.endpoints?.accelerate?.connectionString - ?? null; +function extractConnectionString( + connection: RawDatabaseConnectionRecord, +): string | null { + return ( + connection.endpoints?.pooled?.connectionString ?? + connection.connectionString ?? + connection.endpoints?.direct?.connectionString ?? + connection.endpoints?.accelerate?.connectionString ?? + null + ); } function databaseApiError( @@ -334,8 +423,12 @@ function databaseApiError( code: error?.error?.code ?? "DATABASE_API_ERROR", domain: "database", 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/lib/git/local-branch.ts b/packages/cli/src/lib/git/local-branch.ts index 0e99579..947c2d3 100644 --- a/packages/cli/src/lib/git/local-branch.ts +++ b/packages/cli/src/lib/git/local-branch.ts @@ -1,7 +1,10 @@ import { access, readFile } from "node:fs/promises"; import path from "node:path"; -export async function readLocalGitBranch(cwd: string, signal: AbortSignal): Promise { +export async function readLocalGitBranch( + cwd: string, + signal: AbortSignal, +): Promise { const gitPath = path.join(cwd, ".git"); const headPath = await resolveGitHeadPath(gitPath, signal); if (!headPath) { @@ -9,7 +12,9 @@ export async function readLocalGitBranch(cwd: string, signal: AbortSignal): Prom } try { - const head = (await readFile(headPath, { encoding: "utf8", signal })).trim(); + const head = ( + await readFile(headPath, { encoding: "utf8", signal }) + ).trim(); const refPrefix = "ref: refs/heads/"; if (head.startsWith(refPrefix)) { return head.slice(refPrefix.length); @@ -22,13 +27,19 @@ export async function readLocalGitBranch(cwd: string, signal: AbortSignal): Prom return null; } -async function resolveGitHeadPath(gitPath: string, signal: AbortSignal): Promise { +async function resolveGitHeadPath( + gitPath: string, + signal: AbortSignal, +): Promise { signal.throwIfAborted(); try { const raw = await readFile(gitPath, { encoding: "utf8", signal }); const prefix = "gitdir:"; if (raw.startsWith(prefix)) { - return path.join(path.resolve(path.dirname(gitPath), raw.slice(prefix.length).trim()), "HEAD"); + return path.join( + path.resolve(path.dirname(gitPath), raw.slice(prefix.length).trim()), + "HEAD", + ); } } catch (error) { if (signal.aborted) throw error; diff --git a/packages/cli/src/lib/git/local-status.ts b/packages/cli/src/lib/git/local-status.ts index 2738d7c..1061990 100644 --- a/packages/cli/src/lib/git/local-status.ts +++ b/packages/cli/src/lib/git/local-status.ts @@ -2,10 +2,17 @@ import { execFile } from "node:child_process"; import type { LocalGitState } from "../../types/diagnostics"; -export async function readLocalGitState(cwd: string, signal: AbortSignal): Promise { +export async function readLocalGitState( + cwd: string, + signal: AbortSignal, +): Promise { signal.throwIfAborted(); - const insideWorkTree = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"], signal); + const insideWorkTree = await runGit( + cwd, + ["rev-parse", "--is-inside-work-tree"], + signal, + ); if (insideWorkTree?.trim() !== "true") { return null; } @@ -23,7 +30,11 @@ export async function readLocalGitState(cwd: string, signal: AbortSignal): Promi }; } -function runGit(cwd: string, args: string[], signal: AbortSignal): Promise { +function runGit( + cwd: string, + args: string[], + signal: AbortSignal, +): Promise { return new Promise((resolve, reject) => { signal.throwIfAborted(); diff --git a/packages/cli/src/lib/project/interactive-setup.ts b/packages/cli/src/lib/project/interactive-setup.ts index c549af9..9934d89 100644 --- a/packages/cli/src/lib/project/interactive-setup.ts +++ b/packages/cli/src/lib/project/interactive-setup.ts @@ -1,8 +1,15 @@ -import type { ProjectSetupSuggestion, ProjectSummary } from "../../types/project"; +import type { + ProjectSetupSuggestion, + ProjectSummary, +} from "../../types/project"; import { usageError } from "../../shell/errors"; import { selectPrompt, textPrompt } from "../../shell/prompt"; import type { CommandContext } from "../../shell/runtime"; -import { inferTargetName, sortProjects, type ProjectCandidate } from "./resolution"; +import { + inferTargetName, + sortProjects, + type ProjectCandidate, +} from "./resolution"; import { toProjectSummary, validateProjectSetupNameText } from "./setup"; type InteractiveProjectSetupChoice = @@ -14,7 +21,9 @@ export interface InteractiveProjectSetupResult { project: ProjectSummary; action: "linked" | "created"; targetName: string; - targetNameSource: "prompt" | ProjectSetupSuggestion["suggestedProjectNameSource"]; + targetNameSource: + | "prompt" + | ProjectSetupSuggestion["suggestedProjectNameSource"]; } export async function promptForProjectSetupChoice(options: { @@ -38,7 +47,9 @@ export async function promptForProjectSetupChoice(options: { message: "Which Project should this directory use?", choices: [ ...sortedProjects.map((project) => ({ - label: duplicateNames.has(project.name) ? `${project.name} (${project.id})` : project.name, + label: duplicateNames.has(project.name) + ? `${project.name} (${project.id})` + : project.name, value: { kind: "project" as const, project }, })), { label: "Create a new Project", value: { kind: "create" as const } }, @@ -65,13 +76,17 @@ export async function promptForProjectSetupChoice(options: { }; } - const suggestedName = await inferTargetName(options.context.runtime.cwd, options.context.runtime.signal); + const suggestedName = await inferTargetName( + options.context.runtime.cwd, + options.context.runtime.signal, + ); const rawName = await textPrompt({ input: options.context.runtime.stdin, output: options.context.runtime.stderr, message: "Project name", placeholder: suggestedName.name, - validate: (value) => validateProjectSetupNameText(value, suggestedName.name), + validate: (value) => + validateProjectSetupNameText(value, suggestedName.name), }); const projectName = rawName.trim() || suggestedName.name; const created = await options.createProject(projectName); diff --git a/packages/cli/src/lib/project/local-pin.ts b/packages/cli/src/lib/project/local-pin.ts index 4126d83..b600835 100644 --- a/packages/cli/src/lib/project/local-pin.ts +++ b/packages/cli/src/lib/project/local-pin.ts @@ -106,7 +106,10 @@ export class LocalResolutionPinWriteFailedError extends TaggedError( operation: "create-directory" | "write-temp-file" | "rename-temp-file"; pinPath: string; }>() { - constructor(operation: "create-directory" | "write-temp-file" | "rename-temp-file", cause: unknown) { + constructor( + operation: "create-directory" | "write-temp-file" | "rename-temp-file", + cause: unknown, + ) { super({ message: `Could not write ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH}.`, cause, @@ -168,7 +171,9 @@ export async function readLocalResolutionPin( const file = yield* Result.await(readLocalResolutionPinFile(cwd, signal)); if (file.kind === "missing") { - return Result.ok({ kind: "missing" } satisfies LocalResolutionPinReadResult); + return Result.ok({ + kind: "missing", + } satisfies LocalResolutionPinReadResult); } const parsed = yield* parseLocalResolutionPin(file.raw); @@ -183,7 +188,9 @@ export async function readLocalResolutionPin( }); } -function ensureLocalResolutionPinReadNotAborted(signal: AbortSignal | undefined): Result { +function ensureLocalResolutionPinReadNotAborted( + signal: AbortSignal | undefined, +): Result { return Result.try({ try: () => signal?.throwIfAborted(), catch: (cause) => new LocalResolutionPinReadAbortedError(cause), @@ -197,19 +204,28 @@ type LocalResolutionPinFileReadResult = async function readLocalResolutionPinFile( cwd: string, signal: AbortSignal | undefined, -): Promise> { +): Promise< + Result< + LocalResolutionPinFileReadResult, + LocalResolutionPinReadAbortedError | UnhandledException + > +> { const readResult = await Result.tryPromise({ try: () => readFile(path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH), { encoding: "utf8", signal, }), - catch: (cause) => signal?.aborted - ? new LocalResolutionPinReadAbortedError(cause) - : new UnhandledException({ cause }), + catch: (cause) => + signal?.aborted + ? new LocalResolutionPinReadAbortedError(cause) + : new UnhandledException({ cause }), }); if (readResult.isErr()) { - if (readResult.error instanceof UnhandledException && (readResult.error.cause as NodeJS.ErrnoException).code === "ENOENT") { + if ( + readResult.error instanceof UnhandledException && + (readResult.error.cause as NodeJS.ErrnoException).code === "ENOENT" + ) { return Result.ok({ kind: "missing" }); } return Result.err(readResult.error); @@ -218,12 +234,15 @@ async function readLocalResolutionPinFile( return Result.ok({ kind: "present", raw: readResult.value }); } -function parseLocalResolutionPin(raw: string): Result { +function parseLocalResolutionPin( + raw: string, +): Result { return Result.try({ try: () => JSON.parse(raw) as unknown, - catch: (cause) => cause instanceof SyntaxError - ? new LocalResolutionPinInvalidJsonError(cause) - : new UnhandledException({ cause }), + catch: (cause) => + cause instanceof SyntaxError + ? new LocalResolutionPinInvalidJsonError(cause) + : new UnhandledException({ cause }), }); } @@ -236,29 +255,35 @@ export async function writeLocalResolutionPin( const prismaDir = path.join(cwd, ".prisma"); yield* ensureLocalResolutionPinWriteNotAborted(signal); // mkdir does not accept AbortSignal; check before the filesystem boundary. - yield* Result.await(writeLocalResolutionPinBoundary( - () => mkdir(prismaDir, { recursive: true }), - "create-directory", - signal, - )); + yield* Result.await( + writeLocalResolutionPinBoundary( + () => mkdir(prismaDir, { recursive: true }), + "create-directory", + signal, + ), + ); const pinPath = path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH); const tmpPath = path.join( prismaDir, `local.${process.pid}.${Date.now()}.tmp`, ); const serialized = yield* serializeLocalResolutionPin(pin); - yield* Result.await(writeLocalResolutionPinBoundary( - () => writeFile(tmpPath, serialized, { encoding: "utf8", signal }), - "write-temp-file", - signal, - )); + yield* Result.await( + writeLocalResolutionPinBoundary( + () => writeFile(tmpPath, serialized, { encoding: "utf8", signal }), + "write-temp-file", + signal, + ), + ); yield* ensureLocalResolutionPinWriteNotAborted(signal); // rename does not accept AbortSignal; check before the filesystem boundary. - yield* Result.await(writeLocalResolutionPinBoundary( - () => rename(tmpPath, pinPath), - "rename-temp-file", - signal, - )); + yield* Result.await( + writeLocalResolutionPinBoundary( + () => rename(tmpPath, pinPath), + "rename-temp-file", + signal, + ), + ); return Result.ok(undefined); }); @@ -278,12 +303,17 @@ export async function ensureLocalResolutionPinGitignore( const existingResult = await Result.tryPromise({ try: () => readFile(gitignorePath, { encoding: "utf8", signal }), - catch: (cause) => signal?.aborted - ? new LocalResolutionPinGitignoreUpdateAbortedError(cause) - : new LocalResolutionPinGitignoreUpdateFailedError("read", cause), + catch: (cause) => + signal?.aborted + ? new LocalResolutionPinGitignoreUpdateAbortedError(cause) + : new LocalResolutionPinGitignoreUpdateFailedError("read", cause), }); if (existingResult.isErr()) { - if (existingResult.error instanceof LocalResolutionPinGitignoreUpdateFailedError && (existingResult.error.cause as NodeJS.ErrnoException).code === "ENOENT") { + if ( + existingResult.error instanceof + LocalResolutionPinGitignoreUpdateFailedError && + (existingResult.error.cause as NodeJS.ErrnoException).code === "ENOENT" + ) { existing = null; } else { return Result.err(existingResult.error); @@ -293,7 +323,11 @@ export async function ensureLocalResolutionPinGitignore( } if (existing === null) { - return writeLocalResolutionPinGitignore(gitignorePath, ".prisma/\n", signal); + return writeLocalResolutionPinGitignore( + gitignorePath, + ".prisma/\n", + signal, + ); } const hasPrismaIgnore = existing @@ -310,14 +344,21 @@ export async function ensureLocalResolutionPinGitignore( return writeLocalResolutionPinGitignore(gitignorePath, next, signal); } -function ensureLocalResolutionPinWriteNotAborted(signal: AbortSignal | undefined): Result { +function ensureLocalResolutionPinWriteNotAborted( + signal: AbortSignal | undefined, +): Result { return Result.try({ try: () => signal?.throwIfAborted(), catch: (cause) => new LocalResolutionPinWriteAbortedError(cause), }); } -function serializeLocalResolutionPin(pin: LocalResolutionPin): Result { +function serializeLocalResolutionPin( + pin: LocalResolutionPin, +): Result< + string, + LocalResolutionPinSerializationError | LocalResolutionPinWriteAbortedError +> { return Result.try({ try: () => `${JSON.stringify(pin, null, 2)}\n`, catch: (cause) => new LocalResolutionPinSerializationError(cause), @@ -328,18 +369,26 @@ function writeLocalResolutionPinBoundary( run: () => Promise, operation: "create-directory" | "write-temp-file" | "rename-temp-file", signal: AbortSignal | undefined, -): Promise> { +): Promise< + Result< + void, + LocalResolutionPinWriteAbortedError | LocalResolutionPinWriteFailedError + > +> { return Result.tryPromise({ try: async () => { await run(); }, - catch: (cause) => signal?.aborted - ? new LocalResolutionPinWriteAbortedError(cause) - : new LocalResolutionPinWriteFailedError(operation, cause), + catch: (cause) => + signal?.aborted + ? new LocalResolutionPinWriteAbortedError(cause) + : new LocalResolutionPinWriteFailedError(operation, cause), }); } -function ensureLocalResolutionPinGitignoreUpdateNotAborted(signal: AbortSignal | undefined): Result { +function ensureLocalResolutionPinGitignoreUpdateNotAborted( + signal: AbortSignal | undefined, +): Result { return Result.try({ try: () => signal?.throwIfAborted(), catch: (cause) => new LocalResolutionPinGitignoreUpdateAbortedError(cause), @@ -350,12 +399,19 @@ function writeLocalResolutionPinGitignore( gitignorePath: string, contents: string, signal: AbortSignal | undefined, -): Promise> { +): Promise< + Result< + void, + | LocalResolutionPinGitignoreUpdateAbortedError + | LocalResolutionPinGitignoreUpdateFailedError + > +> { return Result.tryPromise({ try: () => writeFile(gitignorePath, contents, { encoding: "utf8", signal }), - catch: (cause) => signal?.aborted - ? new LocalResolutionPinGitignoreUpdateAbortedError(cause) - : new LocalResolutionPinGitignoreUpdateFailedError("write", cause), + catch: (cause) => + signal?.aborted + ? new LocalResolutionPinGitignoreUpdateAbortedError(cause) + : new LocalResolutionPinGitignoreUpdateFailedError("write", cause), }); } diff --git a/packages/cli/src/lib/project/resolution.ts b/packages/cli/src/lib/project/resolution.ts index 9b1b42b..66a3739 100644 --- a/packages/cli/src/lib/project/resolution.ts +++ b/packages/cli/src/lib/project/resolution.ts @@ -1,7 +1,12 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; -import { Result, TaggedError, UnhandledException, matchError } from "better-result"; +import { + Result, + TaggedError, + UnhandledException, + matchError, +} from "better-result"; import { formatCommandArgument } from "../../shell/command-arguments"; import { CliError } from "../../shell/errors"; @@ -46,7 +51,9 @@ export class ProjectNotFoundError extends TaggedError("ProjectNotFoundError")<{ } } -export class ProjectAmbiguousError extends TaggedError("ProjectAmbiguousError")<{ +export class ProjectAmbiguousError extends TaggedError( + "ProjectAmbiguousError", +)<{ message: string; projectRef: string | null; matches: ProjectCandidate[]; @@ -74,7 +81,9 @@ export class LocalStateStaleError extends TaggedError("LocalStateStaleError")<{ } } -export class LocalProjectWorkspaceMismatchError extends TaggedError("LocalProjectWorkspaceMismatchError")<{ +export class LocalProjectWorkspaceMismatchError extends TaggedError( + "LocalProjectWorkspaceMismatchError", +)<{ message: string; pinnedWorkspaceId: string; pinnedProjectId: string; @@ -94,13 +103,20 @@ export class LocalProjectWorkspaceMismatchError extends TaggedError("LocalProjec } } -export class ProjectSetupRequiredError extends TaggedError("ProjectSetupRequiredError")<{ +export class ProjectSetupRequiredError extends TaggedError( + "ProjectSetupRequiredError", +)<{ message: string; commandName?: string; suggestion: ProjectSetupSuggestion; }>() { - constructor(options: { commandName?: string; suggestion: ProjectSetupSuggestion }) { - const commandLabel = options.commandName ? `prisma-cli ${options.commandName}` : "this command"; + constructor(options: { + commandName?: string; + suggestion: ProjectSetupSuggestion; + }) { + const commandLabel = options.commandName + ? `prisma-cli ${options.commandName}` + : "this command"; super({ message: `This directory is not linked to a Prisma Project, and ${commandLabel} will not choose one from package or directory names.`, commandName: options.commandName, @@ -134,30 +150,50 @@ export interface ResolveProjectOptions { listProjects(): Promise; } -export async function resolveProjectTarget(options: ResolveProjectOptions): Promise> { +export async function resolveProjectTarget( + options: ResolveProjectOptions, +): Promise> { return Result.gen(async function* () { - const localPin = yield* Result.await(readImplicitLocalPin(options, { allowEnvProjectId: true })); + const localPin = yield* Result.await( + readImplicitLocalPin(options, { allowEnvProjectId: true }), + ); const projects = await options.listProjects(); - const target = yield* Result.await(resolveBoundProjectTarget(options, projects, { allowEnvProjectId: true, localPin })); + const target = yield* Result.await( + resolveBoundProjectTarget(options, projects, { + allowEnvProjectId: true, + localPin, + }), + ); if (target) { return Result.ok(target); } - return Result.err(await projectSetupRequiredError({ - cwd: options.context.runtime.cwd, - projects, - commandName: options.commandName, - signal: options.context.runtime.signal, - })); + return Result.err( + await projectSetupRequiredError({ + cwd: options.context.runtime.cwd, + projects, + commandName: options.commandName, + signal: options.context.runtime.signal, + }), + ); }); } -export async function inspectProjectBinding(options: ResolveProjectOptions): Promise> { +export async function inspectProjectBinding( + options: ResolveProjectOptions, +): Promise> { return Result.gen(async function* () { - const localPin = yield* Result.await(readImplicitLocalPin(options, { allowEnvProjectId: false })); + const localPin = yield* Result.await( + readImplicitLocalPin(options, { allowEnvProjectId: false }), + ); const projects = await options.listProjects(); - const target = yield* Result.await(resolveBoundProjectTarget(options, projects, { allowEnvProjectId: false, localPin })); + const target = yield* Result.await( + resolveBoundProjectTarget(options, projects, { + allowEnvProjectId: false, + localPin, + }), + ); if (target) { return Result.ok(target); @@ -172,21 +208,29 @@ export async function inspectProjectBinding(options: ResolveProjectOptions): Pro resolution: { projectSource: "unbound", }, - ...await buildProjectSetupSuggestion({ + ...(await buildProjectSetupSuggestion({ cwd: options.context.runtime.cwd, projects, commandName: options.commandName ?? "project show", signal: options.context.runtime.signal, - }), + })), } satisfies ProjectShowResult); }); } -export function projectNotFoundError(projectRef: string, workspace: AuthWorkspace): CliError { - return projectResolutionErrorToCliError(new ProjectNotFoundError(projectRef, workspace)); +export function projectNotFoundError( + projectRef: string, + workspace: AuthWorkspace, +): CliError { + return projectResolutionErrorToCliError( + new ProjectNotFoundError(projectRef, workspace), + ); } -function projectNotFoundCliError(projectRef: string, workspace: AuthWorkspace): CliError { +function projectNotFoundCliError( + projectRef: string, + workspace: AuthWorkspace, +): CliError { return new CliError({ code: "PROJECT_NOT_FOUND", domain: "project", @@ -198,11 +242,19 @@ function projectNotFoundCliError(projectRef: string, workspace: AuthWorkspace): }); } -export function projectAmbiguousError(projectRef: string | null, matches: ProjectCandidate[]): CliError { - return projectResolutionErrorToCliError(new ProjectAmbiguousError(projectRef, matches)); +export function projectAmbiguousError( + projectRef: string | null, + matches: ProjectCandidate[], +): CliError { + return projectResolutionErrorToCliError( + new ProjectAmbiguousError(projectRef, matches), + ); } -function projectAmbiguousCliError(projectRef: string | null, matches: ProjectCandidate[]): CliError { +function projectAmbiguousCliError( + projectRef: string | null, + matches: ProjectCandidate[], +): CliError { const firstMatch = matches[0]; const nextSteps = ["prisma-cli project list"]; if (firstMatch) { @@ -220,7 +272,10 @@ function projectAmbiguousCliError(projectRef: string | null, matches: ProjectCan : "Multiple projects matched the current directory context.", fix: "Pass --project to choose the project explicitly.", meta: { - matches: matches.map((project) => ({ id: project.id, name: project.name })), + matches: matches.map((project) => ({ + id: project.id, + name: project.name, + })), }, exitCode: 1, nextSteps, @@ -242,7 +297,10 @@ function localStateStaleCliError(): CliError { pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH, }, exitCode: 1, - nextSteps: ["prisma-cli project list", "prisma-cli project link "], + nextSteps: [ + "prisma-cli project list", + "prisma-cli project link ", + ], }); } @@ -251,7 +309,9 @@ export function localProjectWorkspaceMismatchError(options: { pinnedProjectId: string; activeWorkspace: AuthWorkspace; }): CliError { - return projectResolutionErrorToCliError(new LocalProjectWorkspaceMismatchError(options)); + return projectResolutionErrorToCliError( + new LocalProjectWorkspaceMismatchError(options), + ); } function localProjectWorkspaceMismatchCliError(options: { @@ -273,7 +333,11 @@ function localProjectWorkspaceMismatchCliError(options: { activeWorkspaceName: options.activeWorkspace.name, }, exitCode: 1, - nextSteps: ["prisma-cli auth login", "prisma-cli project list", "prisma-cli project link "], + nextSteps: [ + "prisma-cli auth login", + "prisma-cli project list", + "prisma-cli project link ", + ], }); } @@ -283,17 +347,22 @@ function localProjectWorkspaceMismatchCliError(options: { * propagate as exceptions; callers such as `resolveProjectShowInRealMode` * throw this helper's result, so passthrough variants should keep bubbling. */ -export function projectResolutionErrorToCliError(error: ProjectResolutionError): CliError { +export function projectResolutionErrorToCliError( + error: ProjectResolutionError, +): CliError { return matchError(error, { - ProjectNotFoundError: (error) => projectNotFoundCliError(error.projectRef, error.workspace), - ProjectAmbiguousError: (error) => projectAmbiguousCliError(error.projectRef, error.matches), + ProjectNotFoundError: (error) => + projectNotFoundCliError(error.projectRef, error.workspace), + ProjectAmbiguousError: (error) => + projectAmbiguousCliError(error.projectRef, error.matches), ProjectSetupRequiredError: (error) => projectSetupRequiredCliError(error), LocalStateStaleError: () => localStateStaleCliError(), - LocalProjectWorkspaceMismatchError: (error) => localProjectWorkspaceMismatchCliError({ - pinnedWorkspaceId: error.pinnedWorkspaceId, - pinnedProjectId: error.pinnedProjectId, - activeWorkspace: error.activeWorkspace, - }), + LocalProjectWorkspaceMismatchError: (error) => + localProjectWorkspaceMismatchCliError({ + pinnedWorkspaceId: error.pinnedWorkspaceId, + pinnedProjectId: error.pinnedProjectId, + activeWorkspace: error.activeWorkspace, + }), LocalResolutionPinReadAbortedError: (error) => { throw error; }, @@ -311,7 +380,9 @@ export async function buildProjectSetupSuggestion(options: { }): Promise { const suggestedName = await inferTargetName(options.cwd, options.signal); const candidates = sortProjects( - options.projects.filter((project) => projectMatchesSuggestedName(project, suggestedName.name)), + options.projects.filter((project) => + projectMatchesSuggestedName(project, suggestedName.name), + ), ).map(toProjectSummary); return { @@ -329,10 +400,15 @@ export async function projectSetupRequiredError(options: { signal?: AbortSignal; }): Promise { const suggestion = await buildProjectSetupSuggestion(options); - return new ProjectSetupRequiredError({ commandName: options.commandName, suggestion }); + return new ProjectSetupRequiredError({ + commandName: options.commandName, + suggestion, + }); } -function projectSetupRequiredCliError(error: ProjectSetupRequiredError): CliError { +function projectSetupRequiredCliError( + error: ProjectSetupRequiredError, +): CliError { const suggestion = error.suggestion; return new CliError({ code: "PROJECT_SETUP_REQUIRED", @@ -350,14 +426,17 @@ function projectSetupRequiredCliError(error: ProjectSetupRequiredError): CliErro }); } -export function buildProjectSetupNextActions(options: { - commandName?: string; - suggestedProjectName?: string; - createCommand?: string; - reason?: string; -} = {}): NextAction[] { +export function buildProjectSetupNextActions( + options: { + commandName?: string; + suggestedProjectName?: string; + createCommand?: string; + reason?: string; + } = {}, +): NextAction[] { const recoveryCommands = buildProjectRecoveryCommands(options.commandName); - const linkCommand = recoveryCommands[0] ?? "prisma-cli project link "; + const linkCommand = + recoveryCommands[0] ?? "prisma-cli project link "; const retryCommand = recoveryCommands[1]; const commands = ["prisma-cli project list", ...recoveryCommands]; @@ -365,29 +444,36 @@ export function buildProjectSetupNextActions(options: { { kind: "user-choice", journey: "project-setup", - label: "Ask the user whether to link an existing Project or create a new one", + label: + "Ask the user whether to link an existing Project or create a new one", commands, - reason: options.reason - ?? "This directory is not linked to a Prisma Project. Package and directory names are suggestions only, not a safe Project selection.", + reason: + options.reason ?? + "This directory is not linked to a Prisma Project. Package and directory names are suggestions only, not a safe Project selection.", }, { kind: "run-command", journey: "project-setup", label: "Link the chosen Project", command: linkCommand, - reason: "Linking writes the durable local Project binding for this directory.", + reason: + "Linking writes the durable local Project binding for this directory.", }, ]; - const createCommand = options.createCommand - ?? (options.suggestedProjectName ? `prisma-cli project create ${formatCommandArgument(options.suggestedProjectName)}` : undefined); + const createCommand = + options.createCommand ?? + (options.suggestedProjectName + ? `prisma-cli project create ${formatCommandArgument(options.suggestedProjectName)}` + : undefined); if (createCommand) { actions.push({ kind: "run-command", journey: "project-setup", label: "Create and link a new Project", command: createCommand, - reason: "Use this when the user wants a new Prisma Project instead of an existing one.", + reason: + "Use this when the user wants a new Prisma Project instead of an existing one.", }); } @@ -396,23 +482,33 @@ export function buildProjectSetupNextActions(options: { kind: "run-command", journey: "recover", label: "Retry with an explicit Project", - command: retryCommand ?? `prisma-cli ${options.commandName} --project `, + command: + retryCommand ?? + `prisma-cli ${options.commandName} --project `, }); } return actions; } -export async function readPackageName(cwd: string, signal?: AbortSignal): Promise { +export async function readPackageName( + cwd: string, + signal?: AbortSignal, +): Promise { signal?.throwIfAborted(); try { - const raw = await readFile(path.join(cwd, "package.json"), { encoding: "utf8", signal }); + const raw = await readFile(path.join(cwd, "package.json"), { + encoding: "utf8", + signal, + }); const parsed = JSON.parse(raw) as unknown; if (!parsed || typeof parsed !== "object") { return null; } const packageName = "name" in parsed ? parsed.name : null; - return typeof packageName === "string" && packageName.trim().length > 0 ? packageName.trim() : null; + return typeof packageName === "string" && packageName.trim().length > 0 + ? packageName.trim() + : null; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { return null; @@ -424,7 +520,10 @@ export async function readPackageName(cwd: string, signal?: AbortSignal): Promis } } -export async function inferTargetName(cwd: string, signal?: AbortSignal): Promise { +export async function inferTargetName( + cwd: string, + signal?: AbortSignal, +): Promise { const packageName = await readPackageName(cwd, signal); if (packageName && isValidInferredTargetName(packageName)) { return { @@ -443,10 +542,15 @@ function isValidInferredTargetName(value: string): boolean { return /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value); } -export function sortProjects>(projects: T[]): T[] { +export function sortProjects>( + projects: T[], +): T[] { return projects .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 resolveExplicitProject( @@ -454,7 +558,9 @@ function resolveExplicitProject( projects: ProjectCandidate[], workspace: AuthWorkspace, ): Result { - const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef); + const matches = projects.filter( + (project) => project.id === projectRef || project.name === projectRef, + ); if (matches.length === 1) { return Result.ok(matches[0]); } @@ -464,8 +570,15 @@ function resolveExplicitProject( return Result.err(new ProjectNotFoundError(projectRef, workspace)); } -function projectMatchesSuggestedName(project: ProjectCandidate, suggestedName: string): boolean { - return project.id === suggestedName || project.name === suggestedName || project.slug === suggestedName; +function projectMatchesSuggestedName( + project: ProjectCandidate, + suggestedName: string, +): boolean { + return ( + project.id === suggestedName || + project.name === suggestedName || + project.slug === suggestedName + ); } export async function resolveDurablePlatformMapping(): Promise { @@ -481,25 +594,37 @@ async function resolveBoundProjectTarget( }, ): Promise> { if (options.explicitProject) { - const projectResult = resolveExplicitProject(options.explicitProject, projects, options.workspace); + const projectResult = resolveExplicitProject( + options.explicitProject, + projects, + options.workspace, + ); if (projectResult.isErr()) { return Result.err(projectResult.error); } - return Result.ok(resolvedTarget(options.workspace, projectResult.value, "explicit", { - targetName: options.explicitProject, - targetNameSource: "explicit", - })); + return Result.ok( + resolvedTarget(options.workspace, projectResult.value, "explicit", { + targetName: options.explicitProject, + targetNameSource: "explicit", + }), + ); } if (settings.allowEnvProjectId && options.envProjectId) { - const project = projects.find((candidate) => candidate.id === options.envProjectId); + const project = projects.find( + (candidate) => candidate.id === options.envProjectId, + ); if (!project) { - return Result.err(new ProjectNotFoundError(options.envProjectId, options.workspace)); + return Result.err( + new ProjectNotFoundError(options.envProjectId, options.workspace), + ); } - return Result.ok(resolvedTarget(options.workspace, project, "env", { - targetName: options.envProjectId, - targetNameSource: "env", - })); + return Result.ok( + resolvedTarget(options.workspace, project, "env", { + targetName: options.envProjectId, + targetNameSource: "env", + }), + ); } const localPin = settings.localPin; @@ -508,30 +633,41 @@ async function resolveBoundProjectTarget( } if (localPin.kind === "present") { if (localPin.pin.workspaceId !== options.workspace.id) { - return Result.err(new LocalProjectWorkspaceMismatchError({ - pinnedWorkspaceId: localPin.pin.workspaceId, - pinnedProjectId: localPin.pin.projectId, - activeWorkspace: options.workspace, - })); + return Result.err( + new LocalProjectWorkspaceMismatchError({ + pinnedWorkspaceId: localPin.pin.workspaceId, + pinnedProjectId: localPin.pin.projectId, + activeWorkspace: options.workspace, + }), + ); } - const project = projects.find((candidate) => candidate.id === localPin.pin.projectId); + const project = projects.find( + (candidate) => candidate.id === localPin.pin.projectId, + ); if (!project) { return Result.err(new LocalStateStaleError()); } - return Result.ok(resolvedTarget(options.workspace, project, "local-pin", { - targetName: project.name, - targetNameSource: "local-pin", - })); + return Result.ok( + resolvedTarget(options.workspace, project, "local-pin", { + targetName: project.name, + targetNameSource: "local-pin", + }), + ); } const platformMapping = await resolveDurablePlatformMapping(); - if (platformMapping && platformMapping.workspace.id === options.workspace.id) { - return Result.ok(resolvedTarget(options.workspace, platformMapping, "platform-mapping", { - targetName: platformMapping.name, - targetNameSource: "platform-mapping", - })); + if ( + platformMapping && + platformMapping.workspace.id === options.workspace.id + ) { + return Result.ok( + resolvedTarget(options.workspace, platformMapping, "platform-mapping", { + targetName: platformMapping.name, + targetNameSource: "platform-mapping", + }), + ); } return Result.ok(null); @@ -542,29 +678,44 @@ async function readImplicitLocalPin( settings: { allowEnvProjectId: boolean; }, -): Promise> { - if (options.explicitProject || (settings.allowEnvProjectId && options.envProjectId)) { +): Promise< + Result +> { + if ( + options.explicitProject || + (settings.allowEnvProjectId && options.envProjectId) + ) { return Result.ok(null); } - const localPinResult = await readLocalResolutionPin(options.context.runtime.cwd, options.context.runtime.signal); + const localPinResult = await readLocalResolutionPin( + options.context.runtime.cwd, + options.context.runtime.signal, + ); if (localPinResult.isErr()) { return Result.err(localPinReadErrorToProjectError(localPinResult.error)); } const localPin = localPinResult.value; - if (localPin.kind === "present" && localPin.pin.workspaceId !== options.workspace.id) { - return Result.err(new LocalProjectWorkspaceMismatchError({ - pinnedWorkspaceId: localPin.pin.workspaceId, - pinnedProjectId: localPin.pin.projectId, - activeWorkspace: options.workspace, - })); + if ( + localPin.kind === "present" && + localPin.pin.workspaceId !== options.workspace.id + ) { + return Result.err( + new LocalProjectWorkspaceMismatchError({ + pinnedWorkspaceId: localPin.pin.workspaceId, + pinnedProjectId: localPin.pin.projectId, + activeWorkspace: options.workspace, + }), + ); } return Result.ok(localPin); } -function localPinReadErrorToProjectError(error: LocalResolutionPinReadError): ProjectResolutionError { +function localPinReadErrorToProjectError( + error: LocalResolutionPinReadError, +): ProjectResolutionError { return matchError(error, { LocalResolutionPinInvalidJsonError: () => new LocalStateStaleError(), LocalResolutionPinInvalidShapeError: () => new LocalStateStaleError(), @@ -589,7 +740,9 @@ function resolvedTarget( }; } -function buildProjectRecoveryCommands(commandName: string | undefined): string[] { +function buildProjectRecoveryCommands( + commandName: string | undefined, +): string[] { const commands = ["prisma-cli project link "]; if (commandName) { commands.push(`prisma-cli ${commandName} --project `); @@ -597,7 +750,9 @@ function buildProjectRecoveryCommands(commandName: string | undefined): string[] return commands; } -function toProjectSummary(project: Pick): ProjectSummary { +function toProjectSummary( + project: Pick, +): ProjectSummary { return { id: project.id, name: project.name, diff --git a/packages/cli/src/lib/project/setup.ts b/packages/cli/src/lib/project/setup.ts index dc7e73a..bb99215 100644 --- a/packages/cli/src/lib/project/setup.ts +++ b/packages/cli/src/lib/project/setup.ts @@ -25,7 +25,10 @@ export function isValidProjectSetupName(projectName: string): boolean { return projectName.trim().length > 0; } -export function validateProjectSetupNameText(value: string | undefined, fallback: string): string | undefined { +export function validateProjectSetupNameText( + value: string | undefined, + fallback: string, +): string | undefined { if ((value?.trim() || fallback).trim().length > 0) { return undefined; } @@ -38,7 +41,9 @@ export function resolveProjectForSetup( projects: ProjectCandidate[], workspace: AuthWorkspace, ): ProjectCandidate { - const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef); + const matches = projects.filter( + (project) => project.id === projectRef || project.name === projectRef, + ); if (matches.length === 1) { return matches[0]!; } @@ -55,11 +60,22 @@ export async function bindProjectToDirectory( action: ProjectSetupResult["action"], ): Promise> { return Result.gen(async function* () { - yield* Result.await(writeLocalResolutionPin(context.runtime.cwd, { - workspaceId: workspace.id, - projectId: project.id, - }, context.runtime.signal)); - yield* Result.await(ensureLocalResolutionPinGitignore(context.runtime.cwd, context.runtime.signal)); + yield* Result.await( + writeLocalResolutionPin( + context.runtime.cwd, + { + workspaceId: workspace.id, + projectId: project.id, + }, + context.runtime.signal, + ), + ); + yield* Result.await( + ensureLocalResolutionPinGitignore( + context.runtime.cwd, + context.runtime.signal, + ), + ); return Result.ok({ workspace, @@ -74,7 +90,9 @@ export async function bindProjectToDirectory( }); } -export function projectDirectoryBindingErrorToCliError(error: ProjectDirectoryBindingError): CliError { +export function projectDirectoryBindingErrorToCliError( + error: ProjectDirectoryBindingError, +): CliError { // Temporary during the migration to better-result: remove when command boundaries convert Result errors directly. return matchError(error, { LocalResolutionPinSerializationError: (error) => { @@ -83,23 +101,25 @@ export function projectDirectoryBindingErrorToCliError(error: ProjectDirectoryBi LocalResolutionPinWriteAbortedError: (error) => { throw error; }, - LocalResolutionPinWriteFailedError: (error) => localStateWriteFailedError(error, { - why: `The CLI could not write ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH}.`, - meta: { - pinPath: error.pinPath, - operation: error.operation, - }, - }), + LocalResolutionPinWriteFailedError: (error) => + localStateWriteFailedError(error, { + why: `The CLI could not write ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH}.`, + meta: { + pinPath: error.pinPath, + operation: error.operation, + }, + }), LocalResolutionPinGitignoreUpdateAbortedError: (error) => { throw error; }, - LocalResolutionPinGitignoreUpdateFailedError: (error) => localStateWriteFailedError(error, { - why: "The CLI could not update .gitignore to keep local Project binding state out of git.", - meta: { - gitignorePath: error.gitignorePath, - operation: error.operation, - }, - }), + LocalResolutionPinGitignoreUpdateFailedError: (error) => + localStateWriteFailedError(error, { + why: "The CLI could not update .gitignore to keep local Project binding state out of git.", + meta: { + gitignorePath: error.gitignorePath, + operation: error.operation, + }, + }), }); } @@ -116,11 +136,16 @@ function localStateWriteFailedError( debug: formatDebugDetails(error.cause), meta: options.meta, exitCode: 1, - nextSteps: ["prisma-cli project link ", "prisma-cli app deploy --project "], + nextSteps: [ + "prisma-cli project link ", + "prisma-cli app deploy --project ", + ], }); } -export function toProjectSummary(project: Pick): ProjectSummary { +export function toProjectSummary( + project: Pick, +): ProjectSummary { return { id: project.id, name: project.name, @@ -185,7 +210,11 @@ function extractHttpStatus(error: unknown): number | null { return null; } - const candidate = error as { statusCode?: unknown; status?: unknown; message?: unknown }; + const candidate = error as { + statusCode?: unknown; + status?: unknown; + message?: unknown; + }; if (typeof candidate.statusCode === "number") { return candidate.statusCode; } diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index ebf302b..b74e585 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -43,12 +43,17 @@ export function getCliName(): string { return "prisma-cli"; } -export function detectInvocation(env: NodeJS.ProcessEnv, argv: readonly string[]): VersionInvocation { +export function detectInvocation( + env: NodeJS.ProcessEnv, + argv: readonly string[], +): VersionInvocation { if (env.npm_config_user_agent?.startsWith("bun")) { return "bunx"; } - const normalizedExecPath = env.npm_execpath?.replace(/\\/g, "/").toLowerCase(); + const normalizedExecPath = env.npm_execpath + ?.replace(/\\/g, "/") + .toLowerCase(); const normalizedUserAgent = env.npm_config_user_agent?.toLowerCase(); if ( @@ -73,14 +78,20 @@ export function detectInvocation(env: NodeJS.ProcessEnv, argv: readonly string[] return "bunx"; } - if (entry.includes("/node_modules/.bin/") || /\/prisma-cli(\.cmd|\.exe)?$/.test(entry)) { + if ( + entry.includes("/node_modules/.bin/") || + /\/prisma-cli(\.cmd|\.exe)?$/.test(entry) + ) { return "global"; } return "unknown"; } -export function buildVersionResult(env: NodeJS.ProcessEnv, argv: readonly string[]): VersionResult { +export function buildVersionResult( + env: NodeJS.ProcessEnv, + argv: readonly string[], +): VersionResult { return { cli: { name: getCliName(), diff --git a/packages/cli/src/output/patterns.ts b/packages/cli/src/output/patterns.ts index 9b8d45f..d09916c 100644 --- a/packages/cli/src/output/patterns.ts +++ b/packages/cli/src/output/patterns.ts @@ -62,13 +62,27 @@ export function renderList(input: ListPatternInput, ui: ShellUi): string[] { ); const lines = renderCardTitle(input.descriptor, input.title, ui); - lines.push(renderCardRow(ui, keyWidth, input.parentContext.key, input.parentContext.value)); + lines.push( + renderCardRow( + ui, + keyWidth, + input.parentContext.key, + input.parentContext.value, + ), + ); if (input.items.length === 0) { lines.push(renderPlainCardLine(ui, ui.dim(input.emptyMessage))); } else { for (const item of input.items) { - lines.push(renderCardRow(ui, keyWidth, `⚬ ${item.noun}`, formatListItemValue(ui, item))); + lines.push( + renderCardRow( + ui, + keyWidth, + `⚬ ${item.noun}`, + formatListItemValue(ui, item), + ), + ); } } @@ -101,7 +115,14 @@ export function renderShow(input: ShowPatternInput, ui: ShellUi): string[] { const lines = renderCardTitle(input.descriptor, input.title, ui); for (const field of input.fields) { - lines.push(renderCardRow(ui, keyWidth, field.key, formatValue(ui, field.value, field.tone, field.sensitive))); + lines.push( + renderCardRow( + ui, + keyWidth, + field.key, + formatValue(ui, field.value, field.tone, field.sensitive), + ), + ); } pushReadMore(lines, ui, keyWidth, input.descriptor); @@ -111,17 +132,34 @@ export function renderShow(input: ShowPatternInput, ui: ShellUi): string[] { export function renderMutate(input: MutatePatternInput, ui: ShellUi): string[] { const rows = input.context; - const keyWidth = Math.max(0, ...rows.map((row) => stringWidth(`${row.key}:`)), ...readMoreWidth(input.descriptor)); + const keyWidth = Math.max( + 0, + ...rows.map((row) => stringWidth(`${row.key}:`)), + ...readMoreWidth(input.descriptor), + ); const lines = renderCardTitle(input.descriptor, input.title, ui); for (const row of rows) { - lines.push(renderCardRow(ui, keyWidth, row.key, formatValue(ui, row.value, row.tone, row.sensitive))); + lines.push( + renderCardRow( + ui, + keyWidth, + row.key, + formatValue(ui, row.value, row.tone, row.sensitive), + ), + ); } pushReadMore(lines, ui, keyWidth, input.descriptor); lines.push(""); lines.push(`${ui.warning("◇")} ${input.operationDescription}...`); - lines.push(renderSummaryLine(ui, "success", `Applied ${input.operationCount} operation(s)`)); + lines.push( + renderSummaryLine( + ui, + "success", + `Applied ${input.operationCount} operation(s)`, + ), + ); for (const detail of input.details) { lines.push(` ${detail}`); @@ -143,11 +181,23 @@ export function renderInspect(_input: unknown, _ui: ShellUi): string[] { return []; } -function renderCardTitle(descriptor: CommandDescriptor, title: string, ui: ShellUi): string[] { - return [`${ui.strong(formatDescriptorLabel(descriptor))} ${ui.dim("→")} ${ui.dim(title)}`, ""]; +function renderCardTitle( + descriptor: CommandDescriptor, + title: string, + ui: ShellUi, +): string[] { + return [ + `${ui.strong(formatDescriptorLabel(descriptor))} ${ui.dim("→")} ${ui.dim(title)}`, + "", + ]; } -function renderCardRow(ui: ShellUi, keyWidth: number, key: string, value: string): string { +function renderCardRow( + ui: ShellUi, + keyWidth: number, + key: string, + value: string, +): string { return `${renderCardRail(ui)} ${ui.accent(padDisplay(`${key}:`, keyWidth))} ${value}`; } @@ -163,13 +213,20 @@ function readMoreWidth(descriptor: CommandDescriptor): number[] { return descriptor.docsPath ? [stringWidth("Read more")] : []; } -function pushReadMore(lines: string[], ui: ShellUi, keyWidth: number, descriptor: CommandDescriptor): void { +function pushReadMore( + lines: string[], + ui: ShellUi, + keyWidth: number, + descriptor: CommandDescriptor, +): void { if (!descriptor.docsPath) { return; } lines.push(renderCardDivider(ui)); - lines.push(`${renderCardRail(ui)} ${ui.accent(padDisplay("Read more", keyWidth))} ${ui.link(descriptor.docsPath)}`); + lines.push( + `${renderCardRail(ui)} ${ui.accent(padDisplay("Read more", keyWidth))} ${ui.link(descriptor.docsPath)}`, + ); } function renderCardRail(ui: ShellUi): string { @@ -193,7 +250,12 @@ function renderAnnotation(ui: ShellUi, status: AnnotationStatus): string { return ""; } -function formatValue(ui: ShellUi, value: string, tone: ValueTone = "default", sensitive = false): string { +function formatValue( + ui: ShellUi, + value: string, + tone: ValueTone = "default", + sensitive = false, +): string { const resolvedValue = sensitive ? maskValue(value) : value; if (tone === "success") { diff --git a/packages/cli/src/presenters/app-env.ts b/packages/cli/src/presenters/app-env.ts index aa018df..48dad82 100644 --- a/packages/cli/src/presenters/app-env.ts +++ b/packages/cli/src/presenters/app-env.ts @@ -10,7 +10,10 @@ import type { } from "../types/app-env"; import { renderList, renderShow, serializeList } from "../output/patterns"; import { renderVerboseBlock, type VerboseRow } from "../shell/ui"; -import { renderResolvedProjectContextBlock, stripVerboseContext } from "./verbose-context"; +import { + renderResolvedProjectContextBlock, + stripVerboseContext, +} from "./verbose-context"; function scopeLabel(scope: EnvScopeDescriptor): string { if (scope.kind === "role") { @@ -36,7 +39,11 @@ function listTargetLabel(result: EnvListResult): string { return scopeLabel(result.scope); } -type EnvPresenterResult = EnvAddResult | EnvUpdateResult | EnvListResult | EnvRmResult; +type EnvPresenterResult = + | EnvAddResult + | EnvUpdateResult + | EnvListResult + | EnvRmResult; function renderEnvVerboseBlocks( context: CommandContext, @@ -59,7 +66,9 @@ function renderEnvTargetBlock( context: CommandContext, result: EnvPresenterResult, ): string[] { - return renderVerboseBlock(context.ui, envTargetRows(result), { title: "Env target" }); + return renderVerboseBlock(context.ui, envTargetRows(result), { + title: "Env target", + }); } function envTargetRows(result: EnvPresenterResult): VerboseRow[] { @@ -88,10 +97,22 @@ function envListTargetRows(result: EnvPresenterResult): VerboseRow[] { ? [{ key: "branch", value: result.target.branchName }] : []), ...(result.target.branchId - ? [{ key: "branch id", value: result.target.branchId, tone: "dim" as const }] + ? [ + { + key: "branch id", + value: result.target.branchId, + tone: "dim" as const, + }, + ] : []), ...(result.target.branchExists === false - ? [{ key: "branch state", value: "not created yet", tone: "warning" as const }] + ? [ + { + key: "branch state", + value: "not created yet", + tone: "warning" as const, + }, + ] : []), ]; } @@ -109,7 +130,9 @@ function envFileRows(result: EnvPresenterResult): VerboseRow[] { function envResultKeys(result: EnvPresenterResult): string[] { if ("variables" in result && result.variables) { - return result.variables.map((variable) => variable.key).sort((left, right) => left.localeCompare(right)); + return result.variables + .map((variable) => variable.key) + .sort((left, right) => left.localeCompare(right)); } if ("variable" in result && result.variable) { diff --git a/packages/cli/src/presenters/app.ts b/packages/cli/src/presenters/app.ts index 5aaf715..7f0e286 100644 --- a/packages/cli/src/presenters/app.ts +++ b/packages/cli/src/presenters/app.ts @@ -22,7 +22,10 @@ import { renderList, renderShow, serializeList } from "../output/patterns"; import { renderDeployOutputRows } from "../lib/app/deploy-output"; import { formatDomainFailureFix } from "../lib/app/domain-guidance"; import { renderVerboseBlock, type VerboseRow } from "../shell/ui"; -import { renderResolvedProjectContextBlock, stripVerboseContext } from "./verbose-context"; +import { + renderResolvedProjectContextBlock, + stripVerboseContext, +} from "./verbose-context"; export function renderAppBuild( context: CommandContext, @@ -35,7 +38,11 @@ export function renderAppBuild( descriptor, fields: [ { key: "build type", value: result.buildType }, - { key: "entrypoint", value: result.entrypoint ?? "none", tone: result.entrypoint ? "default" : "dim" }, + { + key: "entrypoint", + value: result.entrypoint ?? "none", + tone: result.entrypoint ? "default" : "dim", + }, { key: "directory", value: result.directory }, ], }, @@ -94,22 +101,31 @@ function renderBranchDatabaseDeploySummary( return [ "", ...renderDeployOutputRows(context.ui, [ - { label: "Database", value: result.branchDatabase.database?.name ?? "created" }, + { + label: "Database", + value: result.branchDatabase.database?.name ?? "created", + }, { label: "Env", value: result.branchDatabase.envVars.join(", "), }, ...(result.branchDatabase.schema - ? [{ - label: "Schema", - value: formatBranchDatabaseSchemaCommand(result.branchDatabase.schema.command), - }] + ? [ + { + label: "Schema", + value: formatBranchDatabaseSchemaCommand( + result.branchDatabase.schema.command, + ), + }, + ] : []), ]), ]; } -function formatBranchDatabaseSchemaCommand(command: "migrate-deploy" | "db-push" | "prisma-next-db-init"): string { +function formatBranchDatabaseSchemaCommand( + command: "migrate-deploy" | "db-push" | "prisma-next-db-init", +): string { switch (command) { case "migrate-deploy": return "prisma migrate deploy"; @@ -132,40 +148,53 @@ function renderDeployResolvedContextBlock( context: CommandContext, result: AppDeployResult, ): string[] { - return renderResolvedProjectContextBlock(context.ui, { - workspace: result.workspace, - project: result.project, - resolution: result.resolution, - branch: { - id: result.branch.id, - name: result.branch.name, - kind: result.branch.kind, + return renderResolvedProjectContextBlock( + context.ui, + { + workspace: result.workspace, + project: result.project, + resolution: result.resolution, + branch: { + id: result.branch.id, + name: result.branch.name, + kind: result.branch.kind, + }, }, - }, { - extraRows: [ - { key: "app", value: result.app.name }, - { key: "app id", value: result.app.id, tone: "dim" }, - { key: "deployment id", value: result.deployment.id, tone: "dim" }, - { key: "deployment status", value: result.deployment.status }, - ...(result.localPin ? [{ key: "local pin", value: result.localPin.path }] : []), - { key: "deploy duration", value: formatDuration(result.durationMs) }, - ], - }); + { + extraRows: [ + { key: "app", value: result.app.name }, + { key: "app id", value: result.app.id, tone: "dim" }, + { key: "deployment id", value: result.deployment.id, tone: "dim" }, + { key: "deployment status", value: result.deployment.status }, + ...(result.localPin + ? [{ key: "local pin", value: result.localPin.path }] + : []), + { key: "deploy duration", value: formatDuration(result.durationMs) }, + ], + }, + ); } function renderDeploySettingsBlock( context: CommandContext, result: AppDeployResult, ): string[] { - return renderVerboseBlock(context.ui, [ - ...deploySettingsRows(result.deploySettings), - ...branchDatabaseRows(result.branchDatabase), - ], { title: "Deploy settings" }); + return renderVerboseBlock( + context.ui, + [ + ...deploySettingsRows(result.deploySettings), + ...branchDatabaseRows(result.branchDatabase), + ], + { title: "Deploy settings" }, + ); } function deploySettingsRows(settings: AppDeploySettings): VerboseRow[] { return [ - { key: "framework", value: `${settings.framework.name} (${settings.framework.buildType})` }, + { + key: "framework", + value: `${settings.framework.name} (${settings.framework.buildType})`, + }, { key: "framework source", value: settings.framework.source, tone: "dim" }, { key: "entrypoint", @@ -186,7 +215,9 @@ function deploySettingsRows(settings: AppDeploySettings): VerboseRow[] { ]; } -function branchDatabaseRows(branchDatabase: AppDeployResult["branchDatabase"]): VerboseRow[] { +function branchDatabaseRows( + branchDatabase: AppDeployResult["branchDatabase"], +): VerboseRow[] { if (!branchDatabase) { return [{ key: "branch db", value: "not configured", tone: "dim" }]; } @@ -194,19 +225,22 @@ function branchDatabaseRows(branchDatabase: AppDeployResult["branchDatabase"]): return [ { key: "branch db", - value: branchDatabase.status === "created" - ? `created${branchDatabase.database ? ` (${branchDatabase.database.name})` : ""}` - : `skipped${branchDatabase.reason ? ` (${branchDatabase.reason})` : ""}`, + value: + branchDatabase.status === "created" + ? `created${branchDatabase.database ? ` (${branchDatabase.database.name})` : ""}` + : `skipped${branchDatabase.reason ? ` (${branchDatabase.reason})` : ""}`, tone: branchDatabase.status === "created" ? "success" : "dim", }, ...(branchDatabase.envVars.length > 0 ? [{ key: "branch db env", value: branchDatabase.envVars.join(", ") }] : []), ...(branchDatabase.schema - ? [{ - key: "branch db schema", - value: `${formatBranchDatabaseSchemaCommand(branchDatabase.schema.command)} (${branchDatabase.schema.source}, ${branchDatabase.schema.path})`, - }] + ? [ + { + key: "branch db schema", + value: `${formatBranchDatabaseSchemaCommand(branchDatabase.schema.command)} (${branchDatabase.schema.source}, ${branchDatabase.schema.path})`, + }, + ] : []), ]; } @@ -238,7 +272,9 @@ export function renderAppListDeploys( }, context.ui, ); - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } @@ -282,11 +318,17 @@ export function renderAppShow( descriptor, fields: [ { key: "project", value: result.projectId }, - { key: "app", value: result.app?.name ?? "not selected", tone: result.app ? "default" : "dim" }, + { + key: "app", + value: result.app?.name ?? "not selected", + tone: result.app ? "default" : "dim", + }, { key: "live deployment", value: result.liveDeployment?.id ?? "none", - tone: result.liveDeployment ? toneForStatus(result.liveDeployment.status) : "dim", + tone: result.liveDeployment + ? toneForStatus(result.liveDeployment.status) + : "dim", }, { key: "live url", @@ -302,7 +344,9 @@ export function renderAppShow( }, context.ui, ); - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } @@ -322,15 +366,31 @@ export function renderAppShowDeploy( fields: [ ...(result.app ? [{ key: "app", value: result.app.name }] : []), { key: "deployment", value: result.deployment.id }, - { key: "status", value: result.deployment.status, tone: toneForStatus(result.deployment.status) }, - ...(result.deployment.url ? [{ key: "url", value: result.deployment.url, tone: "link" as const }] : []), + { + key: "status", + value: result.deployment.status, + tone: toneForStatus(result.deployment.status), + }, + ...(result.deployment.url + ? [ + { + key: "url", + value: result.deployment.url, + tone: "link" as const, + }, + ] + : []), ...(result.deployment.live === null ? [] - : [{ - key: "live", - value: result.deployment.live ? "yes" : "no", - tone: result.deployment.live ? "success" as const : "dim" as const, - }]), + : [ + { + key: "live", + value: result.deployment.live ? "yes" : "no", + tone: result.deployment.live + ? ("success" as const) + : ("dim" as const), + }, + ]), { key: "created", value: result.deployment.createdAt, tone: "dim" }, ], }, @@ -357,12 +417,18 @@ export function renderAppOpen( { key: "project", value: result.projectId }, { key: "app", value: result.app.name }, { key: "url", value: result.url, tone: "link" }, - { key: "opened", value: result.opened ? "yes" : "no", tone: result.opened ? "success" : "dim" }, + { + key: "opened", + value: result.opened ? "yes" : "no", + tone: result.opened ? "success" : "dim", + }, ], }, context.ui, ); - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } @@ -384,7 +450,11 @@ export function renderAppDomainAdd( fields: [ ...domainTargetFields(result), { key: "hostname", value: result.domain.hostname }, - { key: "status", value: result.domain.status, tone: toneForDomainStatus(result.domain.status) }, + { + key: "status", + value: result.domain.status, + tone: toneForDomainStatus(result.domain.status), + }, ...domainDnsFields(result.domain), ], }, @@ -408,10 +478,22 @@ export function renderAppDomainShow( fields: [ ...domainTargetFields(result), { key: "hostname", value: result.domain.hostname }, - { key: "status", value: result.domain.status, tone: toneForDomainStatus(result.domain.status) }, + { + key: "status", + value: result.domain.status, + tone: toneForDomainStatus(result.domain.status), + }, ...domainFailureFields(result.domain), - { key: "cert expires", value: formatOptionalUtcDate(result.domain.certExpiresAt), tone: result.domain.certExpiresAt ? "default" : "dim" }, - { key: "created", value: formatUtcDate(result.domain.createdAt), tone: "dim" }, + { + key: "cert expires", + value: formatOptionalUtcDate(result.domain.certExpiresAt), + tone: result.domain.certExpiresAt ? "default" : "dim", + }, + { + key: "created", + value: formatUtcDate(result.domain.createdAt), + tone: "dim", + }, ...domainDnsFields(result.domain), ], }, @@ -435,7 +517,11 @@ export function renderAppDomainRemove( fields: [ ...domainTargetFields(result), { key: "hostname", value: result.hostname }, - { key: "removed", value: result.removed ? "yes" : "no", tone: result.removed ? "success" : "dim" }, + { + key: "removed", + value: result.removed ? "yes" : "no", + tone: result.removed ? "success" : "dim", + }, ], }, context.ui, @@ -458,7 +544,11 @@ export function renderAppDomainRetry( fields: [ ...domainTargetFields(result), { key: "hostname", value: result.domain.hostname }, - { key: "status", value: result.domain.status, tone: toneForDomainStatus(result.domain.status) }, + { + key: "status", + value: result.domain.status, + tone: toneForDomainStatus(result.domain.status), + }, ...domainFailureFields(result.domain), ...domainDnsFields(result.domain), ], @@ -484,15 +574,33 @@ export function renderAppPromote( { key: "project", value: result.projectId }, { key: "app", value: result.app.name }, { key: "deployment", value: result.deployment.id }, - { key: "status", value: result.deployment.status, tone: toneForStatus(result.deployment.status) }, - ...(result.deployment.url ? [{ key: "url", value: result.deployment.url, tone: "link" as const }] : []), - { key: "live", value: result.deployment.live ? "yes" : "no", tone: result.deployment.live ? "success" : "dim" }, + { + key: "status", + value: result.deployment.status, + tone: toneForStatus(result.deployment.status), + }, + ...(result.deployment.url + ? [ + { + key: "url", + value: result.deployment.url, + tone: "link" as const, + }, + ] + : []), + { + key: "live", + value: result.deployment.live ? "yes" : "no", + tone: result.deployment.live ? "success" : "dim", + }, { key: "created", value: result.deployment.createdAt, tone: "dim" }, ], }, context.ui, ); - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } @@ -513,18 +621,42 @@ export function renderAppRollback( { key: "project", value: result.projectId }, { key: "app", value: result.app.name }, { key: "deployment", value: result.deployment.id }, - { key: "status", value: result.deployment.status, tone: toneForStatus(result.deployment.status) }, - ...(result.deployment.url ? [{ key: "url", value: result.deployment.url, tone: "link" as const }] : []), - { key: "live", value: result.deployment.live ? "yes" : "no", tone: result.deployment.live ? "success" : "dim" }, + { + key: "status", + value: result.deployment.status, + tone: toneForStatus(result.deployment.status), + }, + ...(result.deployment.url + ? [ + { + key: "url", + value: result.deployment.url, + tone: "link" as const, + }, + ] + : []), + { + key: "live", + value: result.deployment.live ? "yes" : "no", + tone: result.deployment.live ? "success" : "dim", + }, { key: "created", value: result.deployment.createdAt, tone: "dim" }, ...(result.previousLiveDeploymentId - ? [{ key: "replaced", value: result.previousLiveDeploymentId, tone: "dim" as const }] + ? [ + { + key: "replaced", + value: result.previousLiveDeploymentId, + tone: "dim" as const, + }, + ] : []), ], }, context.ui, ); - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } @@ -556,12 +688,18 @@ export function renderAppRemove( fields: [ { key: "project", value: result.projectId }, { key: "app", value: result.app.name }, - { key: "removed", value: result.removed ? "yes" : "no", tone: result.removed ? "success" : "dim" }, + { + key: "removed", + value: result.removed ? "yes" : "no", + tone: result.removed ? "success" : "dim", + }, ], }, context.ui, ); - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } @@ -569,12 +707,18 @@ export function serializeAppRemove(result: AppRemoveResult) { return stripVerboseContext(result); } -function toneForStatus(status: string): "success" | "warning" | "error" | "default" { +function toneForStatus( + status: string, +): "success" | "warning" | "error" | "default" { if (status === "running" || status === "ready" || status === "healthy") { return "success"; } - if (status === "provisioning" || status === "building" || status === "starting") { + if ( + status === "provisioning" || + status === "building" || + status === "starting" + ) { return "warning"; } @@ -585,7 +729,9 @@ function toneForStatus(status: string): "success" | "warning" | "error" | "defau return "default"; } -function toneForDomainStatus(status: AppDomainStatus): "success" | "warning" | "error" | "default" { +function toneForDomainStatus( + status: AppDomainStatus, +): "success" | "warning" | "error" | "default" { if (status === "active") { return "success"; } @@ -594,14 +740,21 @@ function toneForDomainStatus(status: AppDomainStatus): "success" | "warning" | " return "error"; } - if (status === "pending_dns" || status === "verifying" || status === "provisioning_tls" || status === "verified_routing_blocked") { + if ( + status === "pending_dns" || + status === "verifying" || + status === "provisioning_tls" || + status === "verified_routing_blocked" + ) { return "warning"; } return "default"; } -function domainTargetFields(result: Pick) { +function domainTargetFields( + result: Pick, +) { return [ { key: "workspace", value: result.workspace.name }, { key: "project", value: result.project.name }, @@ -610,23 +763,31 @@ function domainTargetFields(result: Pick) { +function domainDnsFields( + domain: Pick, +) { const records = domain.dnsRecords; if (records.length === 0) { - return [{ - key: "dns record", - value: "not provided by platform", - tone: "dim" as const, - }]; + return [ + { + key: "dns record", + value: "not provided by platform", + tone: "dim" as const, + }, + ]; } - return [{ - key: "dns record", - value: records.map((record) => { - const ttl = record.ttl ? ` ttl ${record.ttl}` : ""; - return `${record.type} ${record.name} -> ${record.value}${ttl}`; - }).join(", "), - }]; + return [ + { + key: "dns record", + value: records + .map((record) => { + const ttl = record.ttl ? ` ttl ${record.ttl}` : ""; + return `${record.type} ${record.name} -> ${record.value}${ttl}`; + }) + .join(", "), + }, + ]; } function formatDomainFailure(domain: AppDomainShowResult["domain"]): string { @@ -634,7 +795,9 @@ function formatDomainFailure(domain: AppDomainShowResult["domain"]): string { return domain.failureCategory ?? "none"; } - return domain.failureCategory ? `${domain.failureCategory} - ${domain.failureReason}` : domain.failureReason; + return domain.failureCategory + ? `${domain.failureCategory} - ${domain.failureReason}` + : domain.failureReason; } function domainFailureFields(domain: AppDomainShowResult["domain"]) { @@ -674,7 +837,9 @@ function formatUtcDate(value: string): string { return `${year}-${month}-${day} ${hours}:${minutes} UTC`; } -function formatRecentDeployments(deployments: AppShowResult["recentDeployments"]): string { +function formatRecentDeployments( + deployments: AppShowResult["recentDeployments"], +): string { if (deployments.length === 0) { return "none"; } diff --git a/packages/cli/src/presenters/auth.ts b/packages/cli/src/presenters/auth.ts index d6b45b3..8fd3ee0 100644 --- a/packages/cli/src/presenters/auth.ts +++ b/packages/cli/src/presenters/auth.ts @@ -89,7 +89,9 @@ function authUserLabel(result: AuthStateResult): string | null { return result.user?.email ?? credentialUserLabel(result); } -function authUserRows(result: AuthStateResult): Parameters[0]["fields"] { +function authUserRows( + result: AuthStateResult, +): Parameters[0]["fields"] { const userLabel = authUserLabel(result); return userLabel ? [{ key: "user", value: userLabel }] : []; } diff --git a/packages/cli/src/presenters/branch.ts b/packages/cli/src/presenters/branch.ts index 0533907..47a9bc5 100644 --- a/packages/cli/src/presenters/branch.ts +++ b/packages/cli/src/presenters/branch.ts @@ -11,7 +11,10 @@ export function renderBranchList( result: BranchListResult, ): string[] { const ui = context.ui; - const lines = [`${ui.strong(formatDescriptorLabel(descriptor))} ${ui.dim("→")} ${ui.dim("Listing branches for the resolved project.")}`, ""]; + const lines = [ + `${ui.strong(formatDescriptorLabel(descriptor))} ${ui.dim("→")} ${ui.dim("Listing branches for the resolved project.")}`, + "", + ]; const rail = ui.dim("│"); lines.push(`${rail} ${ui.accent("project:")} ${result.projectName}`); lines.push(rail); @@ -23,13 +26,26 @@ export function renderBranchList( } const widths = [ - Math.max("Name".length, ...result.branches.map((branch) => branch.name.length)), - Math.max("Role".length, ...result.branches.map((branch) => branch.role.length)), - Math.max("Env map".length, ...result.branches.map((branch) => branch.envMap.length)), + Math.max( + "Name".length, + ...result.branches.map((branch) => branch.name.length), + ), + Math.max( + "Role".length, + ...result.branches.map((branch) => branch.role.length), + ), + Math.max( + "Env map".length, + ...result.branches.map((branch) => branch.envMap.length), + ), ]; - lines.push(`${rail} ${ui.accent(formatColumns(["Name", "Role", "Env map"], widths))}`); + lines.push( + `${rail} ${ui.accent(formatColumns(["Name", "Role", "Env map"], widths))}`, + ); for (const branch of result.branches) { - lines.push(`${rail} ${formatColumns([branch.name, branch.role, branch.envMap], widths)}`); + lines.push( + `${rail} ${formatColumns([branch.name, branch.role, branch.envMap], widths)}`, + ); } lines.push(...renderBranchResolvedContextBlock(context, result)); diff --git a/packages/cli/src/presenters/database.ts b/packages/cli/src/presenters/database.ts index 4642baf..f1b1b92 100644 --- a/packages/cli/src/presenters/database.ts +++ b/packages/cli/src/presenters/database.ts @@ -13,7 +13,10 @@ import type { DatabaseShowResult, DatabaseSummary, } from "../types/database"; -import { renderResolvedProjectContextBlock, stripVerboseContext } from "./verbose-context"; +import { + renderResolvedProjectContextBlock, + stripVerboseContext, +} from "./verbose-context"; export function renderDatabaseList( context: CommandContext, @@ -34,7 +37,9 @@ export function renderDatabaseList( if (result.databases.length === 0) { lines.push(`${rail} ${ui.dim("No databases found.")}`); - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } @@ -53,12 +58,16 @@ export function renderDatabaseList( Math.max("Id".length, ...rows.map((row) => row[4].length)), ]; - lines.push(`${rail} ${ui.accent(formatColumns(["Name", "Branch", "Region", "Status", "Id"], widths))}`); + lines.push( + `${rail} ${ui.accent(formatColumns(["Name", "Branch", "Region", "Status", "Id"], widths))}`, + ); for (const row of rows) { lines.push(`${rail} ${formatColumns(row, widths)}`); } - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } @@ -95,15 +104,25 @@ export function renderDatabaseShow( { key: "project", value: result.projectName }, { key: "database", value: result.database.name }, { key: "id", value: result.database.id, tone: "dim" }, - { key: "branch", value: result.database.branchName ?? "unscoped", tone: result.database.branchName ? "default" : "dim" }, - { key: "region", value: result.database.region ?? "unknown", tone: result.database.region ? "default" : "dim" }, + { + key: "branch", + value: result.database.branchName ?? "unscoped", + tone: result.database.branchName ? "default" : "dim", + }, + { + key: "region", + value: result.database.region ?? "unknown", + tone: result.database.region ? "default" : "dim", + }, { key: "status", value: formatStatus(result.database) }, { key: "connections", value: String(result.connections.length) }, ], }, context.ui, ); - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } @@ -111,7 +130,11 @@ export function serializeDatabaseShow(result: DatabaseShowResult) { return stripVerboseContext(result); } -export function renderDatabaseCreateStdout(_context: CommandContext, _descriptor: CommandDescriptor, result: DatabaseCreateResult): string[] { +export function renderDatabaseCreateStdout( + _context: CommandContext, + _descriptor: CommandDescriptor, + result: DatabaseCreateResult, +): string[] { return [result.connectionString]; } @@ -123,7 +146,11 @@ export function renderDatabaseCreate( const ui = context.ui; const lines = [ "Creating database...", - renderSummaryLine(ui, "success", `Created database "${result.database.name}" in ${formatDatabaseTarget(result.projectName, result.database.branchName)}.`), + renderSummaryLine( + ui, + "success", + `Created database "${result.database.name}" in ${formatDatabaseTarget(result.projectName, result.database.branchName)}.`, + ), " The connection URL below is shown once, so save it now.", ]; @@ -159,7 +186,9 @@ export function renderDatabaseRemove( }, context.ui, ); - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } @@ -183,7 +212,9 @@ export function renderDatabaseConnectionList( if (result.connections.length === 0) { lines.push(`${rail} ${ui.dim("No database connections found.")}`); - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } @@ -198,16 +229,22 @@ export function renderDatabaseConnectionList( Math.max("Created".length, ...rows.map((row) => row[2].length)), ]; - lines.push(`${rail} ${ui.accent(formatColumns(["Name", "Id", "Created"], widths))}`); + lines.push( + `${rail} ${ui.accent(formatColumns(["Name", "Id", "Created"], widths))}`, + ); for (const row of rows) { lines.push(`${rail} ${formatColumns(row, widths)}`); } - lines.push(...renderResolvedProjectContextBlock(context.ui, result.verboseContext)); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, result.verboseContext), + ); return lines; } -export function serializeDatabaseConnectionList(result: DatabaseConnectionListResult) { +export function serializeDatabaseConnectionList( + result: DatabaseConnectionListResult, +) { return { ...serializeList({ context: { @@ -243,7 +280,11 @@ export function renderDatabaseConnectionCreate( const ui = context.ui; const lines = [ "Creating connection...", - renderSummaryLine(ui, "success", `Added a connection to "${result.database.name}" in ${formatDatabaseTarget(result.projectName, result.database.branchName)}.`), + renderSummaryLine( + ui, + "success", + `Added a connection to "${result.database.name}" in ${formatDatabaseTarget(result.projectName, result.database.branchName)}.`, + ), " The connection URL below is shown once, so save it now.", ]; @@ -255,7 +296,9 @@ export function renderDatabaseConnectionCreate( return lines; } -export function serializeDatabaseConnectionCreate(result: DatabaseConnectionCreateResult) { +export function serializeDatabaseConnectionCreate( + result: DatabaseConnectionCreateResult, +) { return stripVerboseContext(result); } @@ -273,13 +316,17 @@ export function renderDatabaseConnectionRemove( ], operationDescription: "Removing database connection", operationCount: 1, - details: ["The connection metadata was removed. Existing one-time secrets were not shown."], + details: [ + "The connection metadata was removed. Existing one-time secrets were not shown.", + ], }, context.ui, ); } -export function serializeDatabaseConnectionRemove(result: DatabaseConnectionRemoveResult) { +export function serializeDatabaseConnectionRemove( + result: DatabaseConnectionRemoveResult, +) { return result; } @@ -287,42 +334,79 @@ function formatStatus(database: DatabaseSummary): string { return database.status ?? (database.isDefault ? "default" : "unknown"); } -function formatDatabaseTarget(projectName: string, branchName: string | null): string { +function formatDatabaseTarget( + projectName: string, + branchName: string | null, +): string { return branchName ? `${projectName} / ${branchName}` : projectName; } -function renderDatabaseCreateVerboseRows(context: CommandContext, result: DatabaseCreateResult): string[] { +function renderDatabaseCreateVerboseRows( + context: CommandContext, + result: DatabaseCreateResult, +): string[] { const rows = [ ...renderWorkspaceProjectRows(result), ["branch", result.database.branchName ?? "unscoped"], - ["database", formatResourceWithId(context, result.database.name, result.database.id)], + [ + "database", + formatResourceWithId(context, result.database.name, result.database.id), + ], ["region", result.database.region ?? "unknown"], ["status", formatStatus(result.database)], - ["connection", formatResourceWithId(context, result.connection.name, result.connection.id)], + [ + "connection", + formatResourceWithId( + context, + result.connection.name, + result.connection.id, + ), + ], ]; return renderMetadataRows(rows); } -function renderDatabaseConnectionCreateVerboseRows(context: CommandContext, result: DatabaseConnectionCreateResult): string[] { +function renderDatabaseConnectionCreateVerboseRows( + context: CommandContext, + result: DatabaseConnectionCreateResult, +): string[] { const rows = [ ...renderWorkspaceProjectRows(result), ["branch", result.database.branchName ?? "unscoped"], - ["database", formatResourceWithId(context, result.database.name, result.database.id)], - ["connection", formatResourceWithId(context, result.connection.name, result.connection.id)], + [ + "database", + formatResourceWithId(context, result.database.name, result.database.id), + ], + [ + "connection", + formatResourceWithId( + context, + result.connection.name, + result.connection.id, + ), + ], ]; return renderMetadataRows(rows); } -function renderWorkspaceProjectRows(result: DatabaseCreateResult | DatabaseConnectionCreateResult): string[][] { +function renderWorkspaceProjectRows( + result: DatabaseCreateResult | DatabaseConnectionCreateResult, +): string[][] { return [ - ...(result.verboseContext ? [["workspace", result.verboseContext.workspace.name]] : []), + ...(result.verboseContext + ? [["workspace", result.verboseContext.workspace.name]] + : []), ["project", result.projectName], ]; } -function formatResourceWithId(context: CommandContext, name: string, id: string): string { +function formatResourceWithId( + context: CommandContext, + name: string, + id: string, +): string { return `${name} ${context.ui.dim(`(${id})`)}`; } diff --git a/packages/cli/src/presenters/project.ts b/packages/cli/src/presenters/project.ts index 56d5725..14ccf8f 100644 --- a/packages/cli/src/presenters/project.ts +++ b/packages/cli/src/presenters/project.ts @@ -14,7 +14,12 @@ import type { ProjectShowResult, } from "../types/project"; import { renderMutate, renderShow, serializeList } from "../output/patterns"; -import { padDisplay, renderNextSteps, renderSummaryLine, renderVerboseBlock } from "../shell/ui"; +import { + padDisplay, + renderNextSteps, + renderSummaryLine, + renderVerboseBlock, +} from "../shell/ui"; import { renderResolvedProjectContextBlock } from "./verbose-context"; export function renderProjectList( @@ -32,27 +37,44 @@ export function renderProjectList( if (result.projects.length === 0) { lines.push(`${rail} ${ui.dim("No projects found.")}`); - if (result.localBinding?.status === "not-linked" || result.localBinding?.status === "invalid") { - lines.push(...renderNextSteps([ - "Link an existing Project you choose: prisma-cli project link ", - "Create a new Project: prisma-cli project create ", - ])); + if ( + result.localBinding?.status === "not-linked" || + result.localBinding?.status === "invalid" + ) { + lines.push( + ...renderNextSteps([ + "Link an existing Project you choose: prisma-cli project link ", + "Create a new Project: prisma-cli project create ", + ]), + ); } return lines; } - const nameWidth = Math.max("name".length, ...result.projects.map((project) => stringWidth(project.name))); + const nameWidth = Math.max( + "name".length, + ...result.projects.map((project) => stringWidth(project.name)), + ); lines.push(rail); - lines.push(`${rail} ${ui.accent(padDisplay("name", nameWidth))} ${ui.accent("id")}`); + lines.push( + `${rail} ${ui.accent(padDisplay("name", nameWidth))} ${ui.accent("id")}`, + ); for (const project of result.projects) { - lines.push(`${rail} ${padDisplay(project.name, nameWidth)} ${project.id}`); + lines.push( + `${rail} ${padDisplay(project.name, nameWidth)} ${project.id}`, + ); } - if (result.localBinding?.status === "not-linked" || result.localBinding?.status === "invalid") { - lines.push(...renderNextSteps([ - "Link an existing Project you choose: prisma-cli project link ", - "Create a new Project: prisma-cli project create ", - ])); + if ( + result.localBinding?.status === "not-linked" || + result.localBinding?.status === "invalid" + ) { + lines.push( + ...renderNextSteps([ + "Link an existing Project you choose: prisma-cli project link ", + "Create a new Project: prisma-cli project create ", + ]), + ); } return lines; @@ -93,17 +115,28 @@ export function renderProjectShow( context.ui, ); - lines.push(...renderVerboseBlock(context.ui, [ - { key: "workspace", value: result.workspace.name }, - { key: "workspace id", value: result.workspace.id, tone: "dim" }, - { key: "project source", value: "unbound" }, - { key: "suggested name", value: `${result.suggestedProjectName} (${result.suggestedProjectNameSource})` }, - ], { title: "Resolved context" })); + lines.push( + ...renderVerboseBlock( + context.ui, + [ + { key: "workspace", value: result.workspace.name }, + { key: "workspace id", value: result.workspace.id, tone: "dim" }, + { key: "project source", value: "unbound" }, + { + key: "suggested name", + value: `${result.suggestedProjectName} (${result.suggestedProjectNameSource})`, + }, + ], + { title: "Resolved context" }, + ), + ); - lines.push(...renderNextSteps([ - "Link an existing Project you choose: prisma-cli project link ", - `Create a new Project: prisma-cli project create ${formatCommandArgument(result.suggestedProjectName)}`, - ])); + lines.push( + ...renderNextSteps([ + "Link an existing Project you choose: prisma-cli project link ", + `Create a new Project: prisma-cli project create ${formatCommandArgument(result.suggestedProjectName)}`, + ]), + ); return lines; } @@ -120,12 +153,23 @@ export function renderProjectSetup( _descriptor: CommandDescriptor, result: ProjectSetupResult, ): string[] { - const lines = result.action === "created" - ? [renderSummaryLine(context.ui, "success", `Created Project "${result.project.name}"`)] - : []; + const lines = + result.action === "created" + ? [ + renderSummaryLine( + context.ui, + "success", + `Created Project "${result.project.name}"`, + ), + ] + : []; lines.push( - renderSummaryLine(context.ui, "success", `Linked "${result.directory}" to Project "${result.project.name}"`), + renderSummaryLine( + context.ui, + "success", + `Linked "${result.directory}" to Project "${result.project.name}"`, + ), `Saved ${result.localPin.path}`, ); @@ -172,11 +216,16 @@ export function renderGitDisconnect( context: [ { key: "project", value: result.project.name }, { key: "workspace", value: result.workspace.name }, - { key: "repository", value: result.repositoryConnection.repository.fullName }, + { + key: "repository", + value: result.repositoryConnection.repository.fullName, + }, ], operationDescription: "Applying repository disconnection", operationCount: 1, - details: ["GitHub branch automation is no longer active for this project."], + details: [ + "GitHub branch automation is no longer active for this project.", + ], }, context.ui, ); @@ -203,11 +252,13 @@ function renderBoundProjectShow( lines.push(`${rail} ${ui.dim("→")} ${ui.link(result.project.url)}`); } - lines.push(...renderResolvedProjectContextBlock(context.ui, { - workspace: result.workspace, - project: result.project, - resolution: result.resolution, - })); + lines.push( + ...renderResolvedProjectContextBlock(context.ui, { + workspace: result.workspace, + project: result.project, + resolution: result.resolution, + }), + ); return lines; } @@ -216,7 +267,10 @@ function formatLocalRepoPath(cwd: string, env: NodeJS.ProcessEnv): string { const resolved = path.resolve(cwd); const home = env.HOME ? path.resolve(env.HOME) : null; - if (home && (resolved === home || resolved.startsWith(`${home}${path.sep}`))) { + if ( + home && + (resolved === home || resolved.startsWith(`${home}${path.sep}`)) + ) { const relative = path.relative(home, resolved); return relative ? `~/${relative}` : "~"; } @@ -224,7 +278,9 @@ function formatLocalRepoPath(cwd: string, env: NodeJS.ProcessEnv): string { return resolved; } -function formatGitConnectionDetail(status: GitRepositoryConnection["status"]): string { +function formatGitConnectionDetail( + status: GitRepositoryConnection["status"], +): string { switch (status) { case "active": return "GitHub branch automation is active for this project."; diff --git a/packages/cli/src/presenters/verbose-context.ts b/packages/cli/src/presenters/verbose-context.ts index 7099d71..1ae49ea 100644 --- a/packages/cli/src/presenters/verbose-context.ts +++ b/packages/cli/src/presenters/verbose-context.ts @@ -24,27 +24,42 @@ export function renderResolvedProjectContextBlock( return []; } - return renderVerboseBlock(ui, [ - ...projectResolutionRows(context), - ...(options.extraRows ?? []), - ], { title: options.title ?? "Resolved context" }); + return renderVerboseBlock( + ui, + [...projectResolutionRows(context), ...(options.extraRows ?? [])], + { title: options.title ?? "Resolved context" }, + ); } -export function projectResolutionRows(context: ResolvedProjectContext): VerboseRow[] { +export function projectResolutionRows( + context: ResolvedProjectContext, +): VerboseRow[] { return [ { key: "workspace", value: context.workspace.name }, { key: "workspace id", value: context.workspace.id, tone: "dim" }, { key: "project", value: context.project.name }, { key: "project id", value: context.project.id, tone: "dim" }, - { key: "project source", value: formatProjectSource(context.resolution.projectSource) }, + { + key: "project source", + value: formatProjectSource(context.resolution.projectSource), + }, ...(context.resolution.targetName ? [{ key: "target name", value: formatTargetName(context.resolution) }] : []), ...(context.branch ? [ - { key: "branch", value: `${context.branch.name} (${context.branch.kind})` }, + { + key: "branch", + value: `${context.branch.name} (${context.branch.kind})`, + }, ...(context.branch.id - ? [{ key: "branch id", value: context.branch.id, tone: "dim" as const }] + ? [ + { + key: "branch id", + value: context.branch.id, + tone: "dim" as const, + }, + ] : []), ] : []), @@ -58,7 +73,9 @@ export function stripVerboseContext( return serialized; } -function formatProjectSource(source: ProjectResolution["projectSource"]): string { +function formatProjectSource( + source: ProjectResolution["projectSource"], +): string { switch (source) { case "explicit": return "--project"; diff --git a/packages/cli/src/presenters/version.ts b/packages/cli/src/presenters/version.ts index 04abbd1..55f0db5 100644 --- a/packages/cli/src/presenters/version.ts +++ b/packages/cli/src/presenters/version.ts @@ -16,7 +16,11 @@ export function renderVersionSuccess( { key: result.cli.name, value: result.cli.version }, { key: "node", value: result.node.version }, { key: "os", value: `${result.os.platform} ${result.os.arch}` }, - { key: "invocation", value: result.invocation, tone: result.invocation === "unknown" ? "dim" : "default" }, + { + key: "invocation", + value: result.invocation, + tone: result.invocation === "unknown" ? "dim" : "default", + }, ], }, context.ui, diff --git a/packages/cli/src/shell/command-meta.ts b/packages/cli/src/shell/command-meta.ts index 5e40068..68d4bfa 100644 --- a/packages/cli/src/shell/command-meta.ts +++ b/packages/cli/src/shell/command-meta.ts @@ -54,7 +54,11 @@ const DESCRIPTORS: CommandDescriptor[] = [ id: "project", path: ["prisma", "project"], description: "Manage and inspect your Prisma projects", - examples: ["prisma-cli project list", "prisma-cli project link proj_123", "prisma-cli project create my-app"], + examples: [ + "prisma-cli project list", + "prisma-cli project link proj_123", + "prisma-cli project create my-app", + ], }, { id: "app", @@ -75,7 +79,11 @@ const DESCRIPTORS: CommandDescriptor[] = [ id: "database", path: ["prisma", "database"], description: "Manage Prisma Postgres databases for a project", - examples: ["prisma-cli database list", "prisma-cli database create my-db", "prisma-cli database connection create db_123"], + examples: [ + "prisma-cli database list", + "prisma-cli database create my-db", + "prisma-cli database connection create db_123", + ], }, { id: "git", @@ -93,19 +101,29 @@ const DESCRIPTORS: CommandDescriptor[] = [ id: "project.show", path: ["prisma", "project", "show"], description: "Show this directory's Project binding", - examples: ["prisma-cli project show", "prisma-cli project show --project proj_123 --json"], + examples: [ + "prisma-cli project show", + "prisma-cli project show --project proj_123 --json", + ], }, { id: "project.create", path: ["prisma", "project", "create"], description: "Create a Project and link this directory", - examples: ["prisma-cli project create my-app", "prisma-cli project create my-app --json"], + examples: [ + "prisma-cli project create my-app", + "prisma-cli project create my-app --json", + ], }, { id: "project.link", path: ["prisma", "project", "link"], description: "Link this directory to a Project", - examples: ["prisma-cli project link", "prisma-cli project link proj_123", "prisma-cli project link \"Acme Dashboard\" --json"], + examples: [ + "prisma-cli project link", + "prisma-cli project link proj_123", + 'prisma-cli project link "Acme Dashboard" --json', + ], }, { id: "git.connect", @@ -121,7 +139,10 @@ const DESCRIPTORS: CommandDescriptor[] = [ id: "git.disconnect", path: ["prisma", "git", "disconnect"], description: "Disconnect the GitHub repository from the resolved project", - examples: ["prisma-cli git disconnect", "prisma-cli git disconnect --project proj_123"], + examples: [ + "prisma-cli git disconnect", + "prisma-cli git disconnect --project proj_123", + ], }, { id: "branch.list", @@ -133,19 +154,30 @@ const DESCRIPTORS: CommandDescriptor[] = [ id: "database.list", path: ["prisma", "database", "list"], description: "List Prisma Postgres databases for the resolved project", - examples: ["prisma-cli database list", "prisma-cli database list --branch feature/foo", "prisma-cli database list --json"], + examples: [ + "prisma-cli database list", + "prisma-cli database list --branch feature/foo", + "prisma-cli database list --json", + ], }, { id: "database.show", path: ["prisma", "database", "show"], description: "Show database metadata without secret values", - examples: ["prisma-cli database show db_123", "prisma-cli database show acme-preview --branch preview --json"], + examples: [ + "prisma-cli database show db_123", + "prisma-cli database show acme-preview --branch preview --json", + ], }, { id: "database.create", path: ["prisma", "database", "create"], - description: "Create a Prisma Postgres database and print its one-time connection URL", - examples: ["prisma-cli database create my-db", "prisma-cli database create my-db --branch feature/foo --region eu-central-1"], + description: + "Create a Prisma Postgres database and print its one-time connection URL", + examples: [ + "prisma-cli database create my-db", + "prisma-cli database create my-db --branch feature/foo --region eu-central-1", + ], }, { id: "database.remove", @@ -167,31 +199,47 @@ const DESCRIPTORS: CommandDescriptor[] = [ id: "database.connection.list", path: ["prisma", "database", "connection", "list"], description: "List database connection metadata without secret values", - examples: ["prisma-cli database connection list db_123", "prisma-cli database connection list acme-preview --branch preview --json"], + examples: [ + "prisma-cli database connection list db_123", + "prisma-cli database connection list acme-preview --branch preview --json", + ], }, { id: "database.connection.create", path: ["prisma", "database", "connection", "create"], - description: "Create a database connection and print its one-time connection URL", - examples: ["prisma-cli database connection create db_123", "prisma-cli database connection create db_123 --name readonly"], + description: + "Create a database connection and print its one-time connection URL", + examples: [ + "prisma-cli database connection create db_123", + "prisma-cli database connection create db_123 --name readonly", + ], }, { id: "database.connection.remove", path: ["prisma", "database", "connection", "remove"], description: "Remove a database connection after exact id confirmation", - examples: ["prisma-cli database connection remove conn_123 --confirm conn_123"], + examples: [ + "prisma-cli database connection remove conn_123 --confirm conn_123", + ], }, { id: "app.build", path: ["prisma", "app", "build"], description: "Build the app locally into a deployable artifact", - examples: ["prisma-cli app build --build-type nextjs", "prisma-cli app build --build-type nuxt", "prisma-cli app build --build-type bun --entry server.ts"], + examples: [ + "prisma-cli app build --build-type nextjs", + "prisma-cli app build --build-type nuxt", + "prisma-cli app build --build-type bun --entry server.ts", + ], }, { id: "app.run", path: ["prisma", "app", "run"], description: "Run your app locally", - examples: ["prisma-cli app run --build-type nextjs", "prisma-cli app run --build-type bun --entry server.ts --port 3000"], + examples: [ + "prisma-cli app run --build-type nextjs", + "prisma-cli app run --build-type bun --entry server.ts --port 3000", + ], }, { id: "app.deploy", @@ -264,19 +312,28 @@ const DESCRIPTORS: CommandDescriptor[] = [ id: "app.domain.wait", path: ["prisma", "app", "domain", "wait"], description: "Wait until a custom domain is active or failed", - examples: ["prisma-cli app domain wait shop.acme.com", "prisma-cli app domain wait shop.acme.com --timeout 0 --json"], + examples: [ + "prisma-cli app domain wait shop.acme.com", + "prisma-cli app domain wait shop.acme.com --timeout 0 --json", + ], }, { id: "app.logs", path: ["prisma", "app", "logs"], description: "Stream logs for the app's current deployment", - examples: ["prisma-cli app logs", "prisma-cli app logs --deployment dep_123"], + examples: [ + "prisma-cli app logs", + "prisma-cli app logs --deployment dep_123", + ], }, { id: "app.list-deploys", path: ["prisma", "app", "list-deploys"], description: "List deployments for the app", - examples: ["prisma-cli app list-deploys", "prisma-cli app list-deploys --app hello-world"], + examples: [ + "prisma-cli app list-deploys", + "prisma-cli app list-deploys --app hello-world", + ], }, { id: "app.show-deploy", @@ -287,20 +344,30 @@ const DESCRIPTORS: CommandDescriptor[] = [ { id: "app.promote", path: ["prisma", "app", "promote"], - description: "Promote a deployment to production by rebuilding with production env vars", - examples: ["prisma-cli app promote dep_123", "prisma-cli app promote dep_123 --app hello-world"], + description: + "Promote a deployment to production by rebuilding with production env vars", + examples: [ + "prisma-cli app promote dep_123", + "prisma-cli app promote dep_123 --app hello-world", + ], }, { id: "app.rollback", path: ["prisma", "app", "rollback"], description: "Roll back production to a previous deployment", - examples: ["prisma-cli app rollback", "prisma-cli app rollback --app hello-world --to dep_123"], + examples: [ + "prisma-cli app rollback", + "prisma-cli app rollback --app hello-world --to dep_123", + ], }, { id: "app.remove", path: ["prisma", "app", "remove"], description: "Remove the app from the current branch", - examples: ["prisma-cli app remove --app hello-world", "prisma-cli app remove --app hello-world --yes"], + examples: [ + "prisma-cli app remove --app hello-world", + "prisma-cli app remove --app hello-world --yes", + ], }, { id: "project.env", @@ -361,13 +428,18 @@ const DESCRIPTORS: CommandDescriptor[] = [ }, ]; -const DESCRIPTORS_BY_ID = new Map(DESCRIPTORS.map((descriptor) => [descriptor.id, descriptor])); +const DESCRIPTORS_BY_ID = new Map( + DESCRIPTORS.map((descriptor) => [descriptor.id, descriptor]), +); type DescriptorCommand = Command & { [COMMAND_DESCRIPTOR_ID]?: string; }; -export function attachCommandDescriptor(command: T, descriptorId: string): T { +export function attachCommandDescriptor( + command: T, + descriptorId: string, +): T { const descriptor = getCommandDescriptor(descriptorId); (command as DescriptorCommand)[COMMAND_DESCRIPTOR_ID] = descriptor.id; command.description(descriptor.description); @@ -392,10 +464,14 @@ export function getDescriptorForCommand(command: Command): CommandDescriptor { } const path = getCommandPath(command); - const descriptor = DESCRIPTORS.find((candidate) => candidate.path.join(" ") === path.join(" ")); + const descriptor = DESCRIPTORS.find( + (candidate) => candidate.path.join(" ") === path.join(" "), + ); if (!descriptor) { - throw new Error(`No command descriptor registered for "${path.join(" ")}".`); + throw new Error( + `No command descriptor registered for "${path.join(" ")}".`, + ); } return descriptor; @@ -417,5 +493,7 @@ export function getCommandPath(command: Command): string[] { } export function formatDescriptorLabel(descriptor: CommandDescriptor): string { - return descriptor.path.length === 1 ? descriptor.path[0] : descriptor.path.slice(1).join(" "); + return descriptor.path.length === 1 + ? descriptor.path[0] + : descriptor.path.slice(1).join(" "); } diff --git a/packages/cli/src/shell/command-runner.ts b/packages/cli/src/shell/command-runner.ts index 07f9d05..e16ddbf 100644 --- a/packages/cli/src/shell/command-runner.ts +++ b/packages/cli/src/shell/command-runner.ts @@ -6,7 +6,14 @@ import { renderCommandDiagnostics } from "./diagnostics-output"; import { authRequiredError, CliError, commandCanceledError } from "./errors"; import { resolveGlobalFlags } from "./global-flags"; import type { CommandSuccess } from "./output"; -import { cliErrorToJson, writeHumanError, writeHumanLines, writeJsonError, writeJsonEvent, writeJsonSuccess } from "./output"; +import { + cliErrorToJson, + writeHumanError, + writeHumanLines, + writeJsonError, + writeJsonEvent, + writeJsonSuccess, +} from "./output"; import { createCommandContext, type CliRuntime } from "./runtime"; interface CommandPresenter { @@ -31,7 +38,9 @@ function toCliError(error: unknown, runtime: CliRuntime): CliError | null { if (error instanceof CliError) return error; if (error instanceof SDKAuthError) { - return authRequiredError(["prisma-cli auth login"], { debug: error.message }); + return authRequiredError(["prisma-cli auth login"], { + debug: error.message, + }); } return null; @@ -49,7 +58,9 @@ export async function runCommand( runtime: CliRuntime, commandName: string, options: Record, - handler: (context: Awaited>) => Promise>, + handler: ( + context: Awaited>, + ) => Promise>, presenter: CommandPresenter, ): Promise { const flags = resolveGlobalFlags(runtime.argv, options); @@ -63,12 +74,15 @@ export async function runCommand( if (flags.json) { writeJsonSuccess(context.output, { ...success, - result: presenter.renderJson ? presenter.renderJson(success.result) : success.result, + result: presenter.renderJson + ? presenter.renderJson(success.result) + : success.result, }); return; } - const stdout = presenter.renderStdout?.(context, descriptor, success.result) ?? []; + const stdout = + presenter.renderStdout?.(context, descriptor, success.result) ?? []; if (flags.quiet) { if (stdout.length > 0) { context.output.stdout.write(`${stdout.join("\n")}\n`); @@ -81,10 +95,7 @@ export async function runCommand( enabled: flags.verbose && rendered.length > 0, durationMs: Date.now() - startedAt, }); - const humanLines = [ - ...rendered, - ...diagnostics, - ]; + const humanLines = [...rendered, ...diagnostics]; if (stdout.length > 0 && humanLines.length > 0) { humanLines.push(""); } @@ -100,7 +111,9 @@ export async function runCommand( if (flags.json) { writeJsonError(context.output, commandName, cliError); } else { - writeHumanError(context.output, context.ui, cliError, { trace: flags.trace }); + writeHumanError(context.output, context.ui, cliError, { + trace: flags.trace, + }); } process.exitCode = cliError.exitCode; @@ -122,7 +135,9 @@ async function renderBestEffortCommandDiagnostics( try { return renderCommandDiagnostics( context, - await collectCommandDiagnostics(context, { durationMs: options.durationMs }), + await collectCommandDiagnostics(context, { + durationMs: options.durationMs, + }), ); } catch { return []; @@ -133,7 +148,9 @@ export async function runStreamingCommand( runtime: CliRuntime, commandName: string, options: Record, - handler: (context: Awaited>) => Promise, + handler: ( + context: Awaited>, + ) => Promise, ): Promise { const flags = resolveGlobalFlags(runtime.argv, options); const context = await createCommandContext(runtime, flags); @@ -166,7 +183,9 @@ export async function runStreamingCommand( nextActions: cliError.nextActions, }); } else { - writeHumanError(context.output, context.ui, cliError, { trace: flags.trace }); + writeHumanError(context.output, context.ui, cliError, { + trace: flags.trace, + }); } process.exitCode = cliError.exitCode; diff --git a/packages/cli/src/shell/diagnostics-output.ts b/packages/cli/src/shell/diagnostics-output.ts index 7044522..cd7c2f4 100644 --- a/packages/cli/src/shell/diagnostics-output.ts +++ b/packages/cli/src/shell/diagnostics-output.ts @@ -17,28 +17,50 @@ export function renderCommandDiagnostics( const { env } = context.runtime; const git = diagnostics.git; - return renderVerboseBlock(context.ui, [ - ...rows, - ...(diagnostics.durationMs === undefined - ? [] - : [{ key: "duration", value: formatDuration(diagnostics.durationMs) }]), - { key: "cwd", value: formatLocalPath(diagnostics.cwd, env) }, - { key: "state file", value: formatLocalPath(diagnostics.stateFilePath, env) }, - ...(git - ? [ - { key: "git ref", value: git.ref ?? "detached", tone: git.ref ? "default" as const : "dim" as const }, - { key: "git sha", value: git.sha ?? "unknown", tone: git.sha ? "default" as const : "dim" as const }, - { key: "git dirty", value: formatDirtyState(git.dirty), tone: git.dirty ? "warning" as const : "dim" as const }, - ] - : [{ key: "git", value: "not detected", tone: "dim" as const }]), - ], { title: options.title ?? "Local context" }); + return renderVerboseBlock( + context.ui, + [ + ...rows, + ...(diagnostics.durationMs === undefined + ? [] + : [{ key: "duration", value: formatDuration(diagnostics.durationMs) }]), + { key: "cwd", value: formatLocalPath(diagnostics.cwd, env) }, + { + key: "state file", + value: formatLocalPath(diagnostics.stateFilePath, env), + }, + ...(git + ? [ + { + key: "git ref", + value: git.ref ?? "detached", + tone: git.ref ? ("default" as const) : ("dim" as const), + }, + { + key: "git sha", + value: git.sha ?? "unknown", + tone: git.sha ? ("default" as const) : ("dim" as const), + }, + { + key: "git dirty", + value: formatDirtyState(git.dirty), + tone: git.dirty ? ("warning" as const) : ("dim" as const), + }, + ] + : [{ key: "git", value: "not detected", tone: "dim" as const }]), + ], + { title: options.title ?? "Local context" }, + ); } export function formatLocalPath(value: string, env: NodeJS.ProcessEnv): string { const resolved = path.resolve(value); const home = env.HOME ? path.resolve(env.HOME) : null; - if (home && (resolved === home || resolved.startsWith(`${home}${path.sep}`))) { + if ( + home && + (resolved === home || resolved.startsWith(`${home}${path.sep}`)) + ) { const relative = path.relative(home, resolved); return relative ? `~/${relative}` : "~"; } diff --git a/packages/cli/src/shell/errors.ts b/packages/cli/src/shell/errors.ts index be923b1..542018b 100644 --- a/packages/cli/src/shell/errors.ts +++ b/packages/cli/src/shell/errors.ts @@ -1,6 +1,12 @@ import type { NextAction } from "./next-actions"; -export type ErrorDomain = "cli" | "auth" | "project" | "branch" | "app" | "database"; +export type ErrorDomain = + | "cli" + | "auth" + | "project" + | "branch" + | "app" + | "database"; export type ErrorSeverity = "error"; export interface CliErrorOptions { @@ -51,9 +57,10 @@ export class CliError extends Error { this.exitCode = options.exitCode ?? 1; this.nextSteps = options.nextSteps ?? []; this.nextActions = options.nextActions ?? []; - this.humanLines = options.humanLines && options.humanLines.length > 0 - ? [...options.humanLines] - : null; + this.humanLines = + options.humanLines && options.humanLines.length > 0 + ? [...options.humanLines] + : null; } } diff --git a/packages/cli/src/shell/global-flags.ts b/packages/cli/src/shell/global-flags.ts index 7273706..45a09f2 100644 --- a/packages/cli/src/shell/global-flags.ts +++ b/packages/cli/src/shell/global-flags.ts @@ -27,9 +27,21 @@ export function addGlobalFlags(command: Command): Command { .option("-v, --verbose", "Increase human-oriented output detail.") .option("--trace", "Show deeper diagnostics for failures.") .option("-y, --yes", "Accept supported confirmation prompts.") - .addOption(new Option("--interactive", "Force interactive behavior when prompts are supported.")) - .addOption(new Option("--no-interactive", "Disable interactive behavior and prompts.")) - .addOption(new Option("--color", "Force color output in supported terminals.")) + .addOption( + new Option( + "--interactive", + "Force interactive behavior when prompts are supported.", + ), + ) + .addOption( + new Option( + "--no-interactive", + "Disable interactive behavior and prompts.", + ), + ) + .addOption( + new Option("--color", "Force color output in supported terminals."), + ) .addOption(new Option("--no-color", "Disable color output.")); } @@ -39,11 +51,20 @@ export function addCompactGlobalFlags(command: Command): Command { .option("-q, --quiet", "Reduce human-oriented output.") .option("-v, --verbose", "Increase human-oriented output detail.") .option("--trace", "Show deeper diagnostics for failures.") - .addOption(new Option("--no-interactive", "Disable interactive behavior and prompts.")) + .addOption( + new Option( + "--no-interactive", + "Disable interactive behavior and prompts.", + ), + ) .option("-y, --yes", "Accept supported confirmation prompts."); } -function getExplicitBoolean(argv: string[], positive: string, negative: string): boolean | undefined { +function getExplicitBoolean( + argv: string[], + positive: string, + negative: string, +): boolean | undefined { for (let index = argv.length - 1; index >= 0; index -= 1) { const value = argv[index]; if (value === positive) { @@ -57,13 +78,20 @@ function getExplicitBoolean(argv: string[], positive: string, negative: string): return undefined; } -export function resolveGlobalFlags(argv: string[], options: Record): GlobalFlags { +export function resolveGlobalFlags( + argv: string[], + options: Record, +): GlobalFlags { return { // Fall back to raw argv scan: Commander v12 can swallow a flag at the parent command // level when both parent and child define the same option name. json: options.json === true || argv.includes("--json"), - quiet: options.quiet === true || argv.includes("--quiet") || argv.includes("-q"), - verbose: options.verbose === true || argv.includes("--verbose") || argv.includes("-v"), + quiet: + options.quiet === true || argv.includes("--quiet") || argv.includes("-q"), + verbose: + options.verbose === true || + argv.includes("--verbose") || + argv.includes("-v"), trace: options.trace === true || argv.includes("--trace"), yes: options.yes === true || argv.includes("--yes") || argv.includes("-y"), interactive: getExplicitBoolean(argv, "--interactive", "--no-interactive"), diff --git a/packages/cli/src/shell/help.ts b/packages/cli/src/shell/help.ts index ac6176a..e230857 100644 --- a/packages/cli/src/shell/help.ts +++ b/packages/cli/src/shell/help.ts @@ -1,7 +1,10 @@ import type { Argument, Command, Option } from "commander"; import { getDescriptorForCommand, formatDescriptorLabel } from "./command-meta"; -import { COMPACT_GLOBAL_OPTION_FLAGS, resolveGlobalFlags } from "./global-flags"; +import { + COMPACT_GLOBAL_OPTION_FLAGS, + resolveGlobalFlags, +} from "./global-flags"; import type { CliRuntime } from "./runtime"; import { createShellUi, padDisplay, wrapText } from "./ui"; @@ -11,11 +14,18 @@ export function renderHelp(command: Command, runtime: CliRuntime): string { const descriptor = getDescriptorForCommand(command); const ui = createShellUi(runtime, resolveGlobalFlags(runtime.argv, {})); const rail = ui.isTTY ? ui.dim("│") : "│"; - const lines = [`${formatDescriptorLabel(descriptor)} ${ui.dim("→")} ${ui.dim(descriptor.description)}`, ""]; + const lines = [ + `${formatDescriptorLabel(descriptor)} ${ui.dim("→")} ${ui.dim(descriptor.description)}`, + "", + ]; const visibleCommands = command.commands.filter( - (candidate) => candidate.name() !== "help" && !(candidate as Command & { hidden?: boolean }).hidden, + (candidate) => + candidate.name() !== "help" && + !(candidate as Command & { hidden?: boolean }).hidden, + ); + const visibleOptions = command.options.filter( + (candidate) => !candidate.hidden, ); - const visibleOptions = command.options.filter((candidate) => !candidate.hidden); if (visibleCommands.length > 0) { lines.push(...renderCommandRows(rail, ui, visibleCommands)); @@ -23,7 +33,10 @@ export function renderHelp(command: Command, runtime: CliRuntime): string { if (descriptor.longDescription) { lines.push(`${rail}`); - const wrapped = wrapText(descriptor.longDescription, Math.max(ui.width - CARD_PREFIX.length, 40)); + const wrapped = wrapText( + descriptor.longDescription, + Math.max(ui.width - CARD_PREFIX.length, 40), + ); for (const line of wrapped) { lines.push(`${rail} ${line}`); } @@ -34,7 +47,12 @@ export function renderHelp(command: Command, runtime: CliRuntime): string { lines.push(`${rail}`); } - if (visibleCommands.length > 0 && visibleOptions.every((option) => COMPACT_GLOBAL_OPTION_FLAGS.includes(option.flags))) { + if ( + visibleCommands.length > 0 && + visibleOptions.every((option) => + COMPACT_GLOBAL_OPTION_FLAGS.includes(option.flags), + ) + ) { lines.push(`${rail} Global options:`); } @@ -51,7 +69,9 @@ export function renderHelp(command: Command, runtime: CliRuntime): string { if (descriptor.docsPath) { lines.push(`${rail}`); - lines.push(`${rail} ${ui.accent(padDisplay("Read more", 16))} ${ui.link(descriptor.docsPath)}`); + lines.push( + `${rail} ${ui.accent(padDisplay("Read more", 16))} ${ui.link(descriptor.docsPath)}`, + ); } lines.push(""); @@ -59,7 +79,11 @@ export function renderHelp(command: Command, runtime: CliRuntime): string { return `${lines.join("\n")}`; } -function renderCommandRows(rail: string, ui: ReturnType, commands: Command[]): string[] { +function renderCommandRows( + rail: string, + ui: ReturnType, + commands: Command[], +): string[] { const rows = commands.map((command) => { const descriptor = getDescriptorForCommand(command); return { @@ -71,7 +95,11 @@ function renderCommandRows(rail: string, ui: ReturnType, c return renderAlignedRows(rail, ui, rows); } -function renderOptionRows(rail: string, ui: ReturnType, options: Option[]): string[] { +function renderOptionRows( + rail: string, + ui: ReturnType, + options: Option[], +): string[] { const rows = options.map((option) => ({ term: option.flags, description: option.description || "", @@ -85,23 +113,41 @@ function renderOptionRows(rail: string, ui: ReturnType, op function renderAlignedRows( rail: string, ui: ReturnType, - rows: Array<{ term: string; description: string; defaultValue?: string | null }>, + rows: Array<{ + term: string; + description: string; + defaultValue?: string | null; + }>, ): string[] { - const termWidth = rows.reduce((width, row) => Math.max(width, row.term.length), 0); - const descriptionWidth = Math.max(ui.width - CARD_PREFIX.length - termWidth - 4, 30); + const termWidth = rows.reduce( + (width, row) => Math.max(width, row.term.length), + 0, + ); + const descriptionWidth = Math.max( + ui.width - CARD_PREFIX.length - termWidth - 4, + 30, + ); const lines: string[] = []; for (const row of rows) { - const wrapped = wrapText(row.description, descriptionWidth, " ".repeat(termWidth + 2)); + const wrapped = wrapText( + row.description, + descriptionWidth, + " ".repeat(termWidth + 2), + ); const [firstLine, ...rest] = wrapped; - lines.push(`${rail} ${ui.accent(padDisplay(row.term, termWidth))} ${firstLine}`); + lines.push( + `${rail} ${ui.accent(padDisplay(row.term, termWidth))} ${firstLine}`, + ); for (const line of rest) { lines.push(`${rail} ${" ".repeat(termWidth)} ${line.trimStart()}`); } if (row.defaultValue) { - lines.push(`${rail} ${" ".repeat(termWidth)} ${ui.dim(`default: ${row.defaultValue}`)}`); + lines.push( + `${rail} ${" ".repeat(termWidth)} ${ui.dim(`default: ${row.defaultValue}`)}`, + ); } } @@ -109,7 +155,9 @@ function renderAlignedRows( } function renderCommandTerm(command: Command): string { - const argumentsList = command.registeredArguments.map(renderArgumentLabel).join(" "); + const argumentsList = command.registeredArguments + .map(renderArgumentLabel) + .join(" "); return `${command.name()}${argumentsList ? ` ${argumentsList}` : ""}`; } diff --git a/packages/cli/src/shell/next-actions.ts b/packages/cli/src/shell/next-actions.ts index 7875184..7b1a9b1 100644 --- a/packages/cli/src/shell/next-actions.ts +++ b/packages/cli/src/shell/next-actions.ts @@ -1,6 +1,14 @@ -export type NextActionKind = "run-command" | "user-choice" | "edit-file" | "done"; +export type NextActionKind = + | "run-command" + | "user-choice" + | "edit-file" + | "done"; -export type NextActionJourney = "project-setup" | "deploy-app" | "inspect" | "recover"; +export type NextActionJourney = + | "project-setup" + | "deploy-app" + | "inspect" + | "recover"; export interface NextAction { kind: NextActionKind; diff --git a/packages/cli/src/shell/output.ts b/packages/cli/src/shell/output.ts index 0f7e06e..d4627c7 100644 --- a/packages/cli/src/shell/output.ts +++ b/packages/cli/src/shell/output.ts @@ -18,11 +18,19 @@ export interface CliOutput { stderr: Writable; } -export function writeJsonSuccess(output: CliOutput, success: CommandSuccess): void { - output.stdout.write(`${JSON.stringify({ ok: true, nextActions: [], ...success }, null, 2)}\n`); +export function writeJsonSuccess( + output: CliOutput, + success: CommandSuccess, +): void { + output.stdout.write( + `${JSON.stringify({ ok: true, nextActions: [], ...success }, null, 2)}\n`, + ); } -export function writeJsonEvent(output: CliOutput, event: Record): void { +export function writeJsonEvent( + output: CliOutput, + event: Record, +): void { output.stdout.write(`${JSON.stringify(event)}\n`); } @@ -41,17 +49,15 @@ export function cliErrorToJson(error: CliError) { } export function formatUnexpectedError(error: unknown, trace: boolean): string { - const debug = error instanceof Error - ? error.stack ?? error.message - : String(error); + const debug = + error instanceof Error ? (error.stack ?? error.message) : String(error); if (trace) { return `${debug}\n`; } - const message = error instanceof Error && error.message - ? error.message - : String(error); + const message = + error instanceof Error && error.message ? error.message : String(error); return [ `Unexpected CLI error: ${message}`, @@ -60,7 +66,11 @@ export function formatUnexpectedError(error: unknown, trace: boolean): string { ].join("\n"); } -export function writeJsonError(output: CliOutput, command: string, error: CliError): void { +export function writeJsonError( + output: CliOutput, + command: string, + error: CliError, +): void { output.stdout.write( `${JSON.stringify( { @@ -103,7 +113,9 @@ export function writeHumanError( return; } - const lines = [renderSummaryLine(ui, "error", `${error.summary} [${error.code}]`)]; + const lines = [ + renderSummaryLine(ui, "error", `${error.summary} [${error.code}]`), + ]; if (error.where) { lines.push(...["", `Where: ${error.where}`]); diff --git a/packages/cli/src/shell/runtime.ts b/packages/cli/src/shell/runtime.ts index 945cd39..aef53f7 100644 --- a/packages/cli/src/shell/runtime.ts +++ b/packages/cli/src/shell/runtime.ts @@ -32,7 +32,10 @@ export interface CommandContext { ui: ShellUi; } -export function configureRuntimeCommand(command: Command, runtime: CliRuntime): Command { +export function configureRuntimeCommand( + command: Command, + runtime: CliRuntime, +): Command { return command .helpCommand(false) .configureHelp({ @@ -56,7 +59,8 @@ export async function createCommandContext( runtime: CliRuntime, flags: GlobalFlags, ): Promise { - const fixturePath = runtime.fixturePath ?? runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH; + const fixturePath = + runtime.fixturePath ?? runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH; const stateDir = resolveStateDir(runtime); // Load the mock API only when fixture mode is explicitly enabled. @@ -87,7 +91,11 @@ export async function createCommandContext( } export function resolveStateDir(runtime: CliRuntime): string { - return runtime.stateDir ?? runtime.env.PRISMA_CLI_STATE_DIR ?? path.join(runtime.cwd, DEFAULT_STATE_DIR_NAME); + return ( + runtime.stateDir ?? + runtime.env.PRISMA_CLI_STATE_DIR ?? + path.join(runtime.cwd, DEFAULT_STATE_DIR_NAME) + ); } export function canPrompt(context: CommandContext): boolean { diff --git a/packages/cli/src/shell/ui.ts b/packages/cli/src/shell/ui.ts index c91e4e8..8c249a9 100644 --- a/packages/cli/src/shell/ui.ts +++ b/packages/cli/src/shell/ui.ts @@ -45,11 +45,17 @@ export interface VerboseRow { const DEFAULT_WIDTH = 80; -export function createShellUi(runtime: CliRuntime, flags: GlobalFlags): ShellUi { +export function createShellUi( + runtime: CliRuntime, + flags: GlobalFlags, +): ShellUi { const isTTY = Boolean(runtime.stderr.isTTY); const colorEnabled = resolveColorEnabled(runtime, flags, isTTY); const colors = createColors({ useColor: colorEnabled }); - const width = runtime.stderr.columns && runtime.stderr.columns > 0 ? runtime.stderr.columns : DEFAULT_WIDTH; + const width = + runtime.stderr.columns && runtime.stderr.columns > 0 + ? runtime.stderr.columns + : DEFAULT_WIDTH; return { isTTY, @@ -83,12 +89,23 @@ export function renderCommandHeader( } const rows = options.rows ?? []; - const lines = [`${ui.strong(options.commandLabel)} ${ui.dim("→")} ${ui.dim(options.description)}`, ""]; + const lines = [ + `${ui.strong(options.commandLabel)} ${ui.dim("→")} ${ui.dim(options.description)}`, + "", + ]; const rail = ui.dim("│"); - const keyWidth = rows.length > 0 ? Math.max(...rows.map((row) => stringWidth(`${row.key}:`)), stringWidth("Read more")) : stringWidth("Read more"); + const keyWidth = + rows.length > 0 + ? Math.max( + ...rows.map((row) => stringWidth(`${row.key}:`)), + stringWidth("Read more"), + ) + : stringWidth("Read more"); for (const row of rows) { - lines.push(`${rail} ${ui.accent(padDisplay(`${row.key}:`, keyWidth))} ${formatHeaderValue(ui, row)}`); + lines.push( + `${rail} ${ui.accent(padDisplay(`${row.key}:`, keyWidth))} ${formatHeaderValue(ui, row)}`, + ); } if (rows.length > 0 || options.docsPath) { @@ -96,7 +113,9 @@ export function renderCommandHeader( } if (options.docsPath) { - lines.push(`${rail} ${ui.accent(padDisplay("Read more", keyWidth))} ${ui.link(options.docsPath)}`); + lines.push( + `${rail} ${ui.accent(padDisplay("Read more", keyWidth))} ${ui.link(options.docsPath)}`, + ); } lines.push(""); @@ -128,7 +147,9 @@ export function renderFieldRows(ui: ShellUi, rows: FieldRow[]): string[] { const keyWidth = Math.max(...rows.map((row) => stringWidth(`${row.key}:`))); return rows.map((row) => { - const key = ui.isTTY ? ui.accent(padDisplay(`${row.key}:`, keyWidth)) : padDisplay(`${row.key}:`, keyWidth); + const key = ui.isTTY + ? ui.accent(padDisplay(`${row.key}:`, keyWidth)) + : padDisplay(`${row.key}:`, keyWidth); const value = row.tone === "dim" ? ui.dim(row.value) : row.value; return `${key} ${value}`; }); @@ -146,7 +167,11 @@ export function renderNextSteps(steps: string[]): string[] { ]; } -export function renderVerboseBlock(ui: ShellUi, rows: VerboseRow[], options: { title?: string } = {}): string[] { +export function renderVerboseBlock( + ui: ShellUi, + rows: VerboseRow[], + options: { title?: string } = {}, +): string[] { if (!ui.verbose || rows.length === 0) { return []; } @@ -158,12 +183,18 @@ export function renderVerboseBlock(ui: ShellUi, rows: VerboseRow[], options: { t return [ "", `${ui.dim(title)}:`, - ...rows.map((row) => `${rail} ${ui.accent(padDisplay(`${row.key}:`, keyWidth))} ${formatVerboseValue(ui, row)}`), + ...rows.map( + (row) => + `${rail} ${ui.accent(padDisplay(`${row.key}:`, keyWidth))} ${formatVerboseValue(ui, row)}`, + ), ]; } export function formatColumns(columns: string[], widths: number[]): string { - return columns.map((value, index) => padDisplay(value, widths[index])).join(" ").trimEnd(); + return columns + .map((value, index) => padDisplay(value, widths[index])) + .join(" ") + .trimEnd(); } export function plain(text: string): string { @@ -182,10 +213,16 @@ export function padDisplay(text: string, width: number): string { } export function maskValue(value: string): string { - return value.replace(/([A-Za-z0-9._%+-]{1,})(?=@)/g, "****").replace(/:\/\/[^:@/\s]+:[^@/\s]+@/g, "://****:****@"); + return value + .replace(/([A-Za-z0-9._%+-]{1,})(?=@)/g, "****") + .replace(/:\/\/[^:@/\s]+:[^@/\s]+@/g, "://****:****@"); } -function resolveColorEnabled(runtime: CliRuntime, flags: GlobalFlags, isTTY: boolean): boolean { +function resolveColorEnabled( + runtime: CliRuntime, + flags: GlobalFlags, + isTTY: boolean, +): boolean { if (flags.color === true) { return true; } diff --git a/packages/cli/src/shell/update-check.ts b/packages/cli/src/shell/update-check.ts index 4afc6a7..833a5cd 100644 --- a/packages/cli/src/shell/update-check.ts +++ b/packages/cli/src/shell/update-check.ts @@ -8,7 +8,8 @@ import { getCliName, getCliVersion } from "../lib/version"; import type { CliRuntime } from "./runtime"; const UPDATE_CHECK_FILE_NAME = "update-check.json"; -const FALLBACK_INSTALL_DOCS_URL = "https://www.prisma.io/docs/orm/tools/prisma-cli"; +const FALLBACK_INSTALL_DOCS_URL = + "https://www.prisma.io/docs/orm/tools/prisma-cli"; const NOTIFICATION_INTERVAL_MS = 24 * 60 * 60 * 1000; const REGISTRY_URL = "https://registry.npmjs.org/@prisma%2fcli"; const REGISTRY_TIMEOUT_MS = 3_000; @@ -35,7 +36,9 @@ export class UpdateCheckStore { async read(): Promise { try { - return JSON.parse(await readFile(this.filePath, "utf8")) as UpdateCheckState; + return JSON.parse( + await readFile(this.filePath, "utf8"), + ) as UpdateCheckState; } catch (error) { if (isUnreadableCacheError(error)) { return null; @@ -47,14 +50,19 @@ export class UpdateCheckStore { async write(state: UpdateCheckState): Promise { const dir = path.dirname(this.filePath); - const tempPath = path.join(dir, `${UPDATE_CHECK_FILE_NAME}.${process.pid}.${randomUUID()}.tmp`); + const tempPath = path.join( + dir, + `${UPDATE_CHECK_FILE_NAME}.${process.pid}.${randomUUID()}.tmp`, + ); await mkdir(dir, { recursive: true }); await writeFile(tempPath, `${JSON.stringify(state, null, 2)}\n`, "utf8"); await rename(tempPath, this.filePath); } } -export async function maybeWriteCachedUpdateNotification(runtime: CliRuntime): Promise { +export async function maybeWriteCachedUpdateNotification( + runtime: CliRuntime, +): Promise { if (!canRunUpdateCheck(runtime)) { return; } @@ -65,8 +73,17 @@ export async function maybeWriteCachedUpdateNotification(runtime: CliRuntime): P const state = await store.read(); const latestVersion = state?.latestVersion; - if (latestVersion && isInstalledVersionStale(getCliVersion(), latestVersion) && shouldNotify(state)) { - runtime.stderr.write(renderUpdateNotification(latestVersion, selectUpdateInstruction(runtime.env))); + if ( + latestVersion && + isInstalledVersionStale(getCliVersion(), latestVersion) && + shouldNotify(state) + ) { + runtime.stderr.write( + renderUpdateNotification( + latestVersion, + selectUpdateInstruction(runtime.env), + ), + ); await store.write({ ...state, packageName: "@prisma/cli", @@ -89,7 +106,10 @@ export async function runUpdateDiscovery(options: { now?: Date; }): Promise { try { - const latestVersion = await fetchLatestVersion(options.registryUrl ?? REGISTRY_URL, options.fetchImpl ?? fetch); + const latestVersion = await fetchLatestVersion( + options.registryUrl ?? REGISTRY_URL, + options.fetchImpl ?? fetch, + ); if (!latestVersion) { return; } @@ -110,10 +130,17 @@ export async function runUpdateDiscovery(options: { function isUnreadableCacheError(error: unknown): boolean { const code = (error as NodeJS.ErrnoException).code; - return code === "ENOENT" || code === "EACCES" || code === "EPERM" || error instanceof SyntaxError; + return ( + code === "ENOENT" || + code === "EACCES" || + code === "EPERM" || + error instanceof SyntaxError + ); } -export async function runUpdateDiscoveryWorker(env: NodeJS.ProcessEnv = process.env): Promise { +export async function runUpdateDiscoveryWorker( + env: NodeJS.ProcessEnv = process.env, +): Promise { const cacheDir = env.PRISMA_CLI_UPDATE_CHECK_DIR; const installedVersion = env.PRISMA_CLI_UPDATE_CHECK_INSTALLED_VERSION; @@ -133,7 +160,10 @@ function canRunUpdateCheck(runtime: CliRuntime): boolean { return false; } - if (isTestRuntime(runtime.env) && runtime.env.PRISMA_CLI_TEST_ENABLE_UPDATE_CHECK !== "1") { + if ( + isTestRuntime(runtime.env) && + runtime.env.PRISMA_CLI_TEST_ENABLE_UPDATE_CHECK !== "1" + ) { return false; } @@ -145,7 +175,11 @@ function canRunUpdateCheck(runtime: CliRuntime): boolean { return false; } - if (runtime.argv.includes("--json") || runtime.argv.includes("--quiet") || runtime.argv.includes("-q")) { + if ( + runtime.argv.includes("--json") || + runtime.argv.includes("--quiet") || + runtime.argv.includes("-q") + ) { return false; } @@ -228,14 +262,20 @@ export function selectUpdateInstruction( } } - if (env.npm_config_global === "true" || isLikelyGlobalNpmEntrypoint(entrypoint)) { + if ( + env.npm_config_global === "true" || + isLikelyGlobalNpmEntrypoint(entrypoint) + ) { return commandInstruction("npm install --global @prisma/cli@latest"); } return docsInstruction(); } -function renderUpdateNotification(latestVersion: string, instruction: UpdateInstruction): string { +function renderUpdateNotification( + latestVersion: string, + instruction: UpdateInstruction, +): string { return [ `Update available: ${getCliName()} ${getCliVersion()} -> ${latestVersion}`, renderUpdateInstruction(instruction), @@ -252,11 +292,19 @@ function renderUpdateInstruction(instruction: UpdateInstruction): string { } function isEphemeralInvocation(entrypoint: string, lifecycle: string): boolean { - return lifecycle === "npx" || lifecycle === "pnpx" || entrypoint.includes("/_npx/") || entrypoint.includes("/.bun/"); + return ( + lifecycle === "npx" || + lifecycle === "pnpx" || + entrypoint.includes("/_npx/") || + entrypoint.includes("/.bun/") + ); } function isLikelyGlobalNpmEntrypoint(entrypoint: string): boolean { - return /\/npm\/prisma-cli(\.cmd|\.exe)?$/.test(entrypoint) || /\/npm-global\/bin\/prisma-cli$/.test(entrypoint); + return ( + /\/npm\/prisma-cli(\.cmd|\.exe)?$/.test(entrypoint) || + /\/npm-global\/bin\/prisma-cli$/.test(entrypoint) + ); } function commandInstruction(value: string): UpdateInstruction { @@ -278,11 +326,13 @@ function resolveUpdateCheckCacheDir(runtime: CliRuntime): string { } if (process.platform === "win32") { - const localAppData = runtime.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local"); + const localAppData = + runtime.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local"); return path.join(localAppData, "prisma-cli", "cache"); } - const xdgCacheHome = runtime.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache"); + const xdgCacheHome = + runtime.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache"); return path.join(xdgCacheHome, "prisma-cli"); } @@ -292,10 +342,16 @@ function isTestRuntime(env: NodeJS.ProcessEnv): boolean { function isAtLeastIntervalAgo(value: string): boolean { const timestamp = Date.parse(value); - return Number.isNaN(timestamp) || Date.now() - timestamp >= NOTIFICATION_INTERVAL_MS; + return ( + Number.isNaN(timestamp) || + Date.now() - timestamp >= NOTIFICATION_INTERVAL_MS + ); } -function isInstalledVersionStale(installedVersion: string, latestVersion: string): boolean { +function isInstalledVersionStale( + installedVersion: string, + latestVersion: string, +): boolean { const installed = parseVersion(installedVersion); const latest = parseVersion(latestVersion); @@ -374,7 +430,10 @@ function comparePrereleasePart(left: string, right: string): number { return left.localeCompare(right); } -async function fetchLatestVersion(registryUrl: string, fetchImpl: typeof fetch): Promise { +async function fetchLatestVersion( + registryUrl: string, + fetchImpl: typeof fetch, +): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), REGISTRY_TIMEOUT_MS); @@ -390,7 +449,9 @@ async function fetchLatestVersion(registryUrl: string, fetchImpl: typeof fetch): return null; } - const metadata = await response.json() as { "dist-tags"?: { latest?: unknown } }; + const metadata = (await response.json()) as { + "dist-tags"?: { latest?: unknown }; + }; const latest = metadata["dist-tags"]?.latest; return typeof latest === "string" ? latest : null; } finally { diff --git a/packages/cli/src/types/app.ts b/packages/cli/src/types/app.ts index eb6c0ab..5b7d6e9 100644 --- a/packages/cli/src/types/app.ts +++ b/packages/cli/src/types/app.ts @@ -161,7 +161,12 @@ export type AppDomainStatus = | "failed" | "removing"; -export type AppDomainFailureCategory = "dns" | "acme" | "storage" | "unknown" | null; +export type AppDomainFailureCategory = + | "dns" + | "acme" + | "storage" + | "unknown" + | null; export interface AppDomainDnsRecord { type: string; diff --git a/packages/cli/src/types/project.ts b/packages/cli/src/types/project.ts index 35288a4..f3582a0 100644 --- a/packages/cli/src/types/project.ts +++ b/packages/cli/src/types/project.ts @@ -18,7 +18,14 @@ export type ProjectSource = export interface ProjectResolution { projectSource: ProjectSource; targetName?: string | null; - targetNameSource?: "explicit" | "env" | "local-pin" | "package-name" | "directory-name" | "platform-mapping" | "prompt"; + targetNameSource?: + | "explicit" + | "env" + | "local-pin" + | "package-name" + | "directory-name" + | "platform-mapping" + | "prompt"; } export interface ProjectLocalBindingState { @@ -55,7 +62,9 @@ export interface UnboundProjectShowResult extends ProjectSetupSuggestion { }; } -export type ProjectShowResult = BoundProjectShowResult | UnboundProjectShowResult; +export type ProjectShowResult = + | BoundProjectShowResult + | UnboundProjectShowResult; export interface ProjectSetupResult { workspace: AuthWorkspace; @@ -94,6 +103,7 @@ export interface GitRepositoryConnection { updatedAt: string | null; } -export interface ProjectRepositoryConnectionResult extends BoundProjectShowResult { +export interface ProjectRepositoryConnectionResult + extends BoundProjectShowResult { repositoryConnection: GitRepositoryConnection; } diff --git a/packages/cli/src/use-cases/auth.ts b/packages/cli/src/use-cases/auth.ts index 0b2b091..4209714 100644 --- a/packages/cli/src/use-cases/auth.ts +++ b/packages/cli/src/use-cases/auth.ts @@ -1,13 +1,20 @@ import { usageError } from "../shell/errors"; import type { AuthProviderId, AuthStateResult } from "../types/auth"; -import type { AuthUseCases, IdentityGateway, LoginSelection, SessionGateway } from "./contracts"; +import type { + AuthUseCases, + IdentityGateway, + LoginSelection, + SessionGateway, +} from "./contracts"; interface AuthUseCaseDependencies { identityGateway: IdentityGateway; sessionGateway: SessionGateway; } -export function createAuthUseCases(dependencies: AuthUseCaseDependencies): AuthUseCases { +export function createAuthUseCases( + dependencies: AuthUseCaseDependencies, +): AuthUseCases { return { whoami: () => resolveCurrentAuthState(dependencies), login: async (selection: LoginSelection) => { @@ -40,7 +47,8 @@ export function createAuthUseCases(dependencies: AuthUseCaseDependencies): AuthU return provider; }, listUsersForProvider: async (providerId: AuthProviderId) => { - const users = dependencies.identityGateway.listUsersForProvider(providerId); + const users = + dependencies.identityGateway.listUsersForProvider(providerId); if (users.length === 0) { throw usageError( @@ -54,8 +62,14 @@ export function createAuthUseCases(dependencies: AuthUseCaseDependencies): AuthU return users; }, - resolveUserForProvider: async (providerId: AuthProviderId, userId: string) => { - const user = dependencies.identityGateway.getUserForProvider(providerId, userId); + resolveUserForProvider: async ( + providerId: AuthProviderId, + userId: string, + ) => { + const user = dependencies.identityGateway.getUserForProvider( + providerId, + userId, + ); if (!user) { throw usageError( @@ -69,9 +83,13 @@ export function createAuthUseCases(dependencies: AuthUseCaseDependencies): AuthU return user; }, - listWorkspacesForUser: async (userId: string) => dependencies.identityGateway.listUserWorkspaces(userId), + listWorkspacesForUser: async (userId: string) => + dependencies.identityGateway.listUserWorkspaces(userId), resolveWorkspaceForUser: async (userId: string, workspaceId: string) => { - const workspace = dependencies.identityGateway.getUserWorkspace(userId, workspaceId); + const workspace = dependencies.identityGateway.getUserWorkspace( + userId, + workspaceId, + ); if (!workspace) { throw usageError( @@ -88,7 +106,9 @@ export function createAuthUseCases(dependencies: AuthUseCaseDependencies): AuthU }; } -async function resolveCurrentAuthState(dependencies: AuthUseCaseDependencies): Promise { +async function resolveCurrentAuthState( + dependencies: AuthUseCaseDependencies, +): Promise { const session = await dependencies.sessionGateway.readAuthSession(); if (!session) { @@ -103,7 +123,9 @@ async function resolveCurrentAuthState(dependencies: AuthUseCaseDependencies): P const provider = dependencies.identityGateway.getProvider(session.provider); const user = dependencies.identityGateway.getUser(session.userId); - const workspace = dependencies.identityGateway.getWorkspace(session.workspaceId); + const workspace = dependencies.identityGateway.getWorkspace( + session.workspaceId, + ); if (!provider || !user || !workspace) { return { diff --git a/packages/cli/src/use-cases/branch.ts b/packages/cli/src/use-cases/branch.ts index 9e97d2f..88e5633 100644 --- a/packages/cli/src/use-cases/branch.ts +++ b/packages/cli/src/use-cases/branch.ts @@ -13,10 +13,13 @@ interface BranchUseCaseDependencies { projectStateGateway: ProjectStateGateway; } -export function createBranchUseCases(dependencies: BranchUseCaseDependencies): BranchUseCases { +export function createBranchUseCases( + dependencies: BranchUseCaseDependencies, +): BranchUseCases { return { list: async (): Promise => { - const projectId = await dependencies.projectStateGateway.readRememberedProjectId(); + const projectId = + await dependencies.projectStateGateway.readRememberedProjectId(); if (!projectId) { return { projectId: "", @@ -25,8 +28,14 @@ export function createBranchUseCases(dependencies: BranchUseCaseDependencies): B }; } - const remoteBranches = await listRemoteBranches(dependencies.branchGateway, projectId); - const projectName = resolveProjectName(dependencies.projectGateway, projectId); + const remoteBranches = await listRemoteBranches( + dependencies.branchGateway, + projectId, + ); + const projectName = resolveProjectName( + dependencies.projectGateway, + projectId, + ); return { projectId, @@ -37,7 +46,10 @@ export function createBranchUseCases(dependencies: BranchUseCaseDependencies): B }; } -function resolveProjectName(projectGateway: ProjectGateway, projectId: string | null): string | null { +function resolveProjectName( + projectGateway: ProjectGateway, + projectId: string | null, +): string | null { if (!projectId) { return null; } @@ -56,28 +68,30 @@ async function listRemoteBranches( return branchGateway.listBranchesForProject(projectId); } -function buildBranchSummaries(remoteBranches: RemoteBranchRecord[]): BranchSummary[] { - return sortBranches(remoteBranches.map((branch) => ({ - id: branch.id, - name: branch.name, - role: branch.role, - envMap: branch.role, - }))); +function buildBranchSummaries( + remoteBranches: RemoteBranchRecord[], +): BranchSummary[] { + return sortBranches( + remoteBranches.map((branch) => ({ + id: branch.id, + name: branch.name, + role: branch.role, + envMap: branch.role, + })), + ); } function sortBranches(branches: BranchSummary[]): BranchSummary[] { - return branches - .slice() - .sort((left, right) => { - const leftRank = branchOrder(left); - const rightRank = branchOrder(right); + return branches.slice().sort((left, right) => { + const leftRank = branchOrder(left); + const rightRank = branchOrder(right); - if (leftRank !== rightRank) { - return leftRank - rightRank; - } + if (leftRank !== rightRank) { + return leftRank - rightRank; + } - return left.name.localeCompare(right.name); - }); + return left.name.localeCompare(right.name); + }); } function branchOrder(branch: BranchSummary): number { diff --git a/packages/cli/src/use-cases/contracts.ts b/packages/cli/src/use-cases/contracts.ts index fd1add1..3e42c18 100644 --- a/packages/cli/src/use-cases/contracts.ts +++ b/packages/cli/src/use-cases/contracts.ts @@ -1,4 +1,9 @@ -import type { AuthProviderId, AuthStateResult, AuthUser, AuthWorkspace } from "../types/auth"; +import type { + AuthProviderId, + AuthStateResult, + AuthUser, + AuthWorkspace, +} from "../types/auth"; import type { BranchListResult, BranchRole } from "../types/branch"; import type { ProjectSummary } from "../types/project"; @@ -43,21 +48,33 @@ export interface IdentityGateway { getProvider(providerId: string): ProviderInfo | undefined; listUsersForProvider(providerId: AuthProviderId): IdentityUser[]; getUser(userId: string): IdentityUser | undefined; - getUserForProvider(providerId: AuthProviderId, userId: string): IdentityUser | undefined; + getUserForProvider( + providerId: AuthProviderId, + userId: string, + ): IdentityUser | undefined; listUserWorkspaces(userId: string): AuthWorkspace[]; getWorkspace(workspaceId: string): AuthWorkspace | undefined; - getUserWorkspace(userId: string, workspaceId: string): AuthWorkspace | undefined; + getUserWorkspace( + userId: string, + workspaceId: string, + ): AuthWorkspace | undefined; } export interface ProjectGateway { listProjectsForWorkspace(workspaceId: string): ProjectRecord[]; getProject(projectId: string): ProjectRecord | undefined; - getProjectForWorkspace(workspaceId: string, projectId: string): ProjectRecord | undefined; + getProjectForWorkspace( + workspaceId: string, + projectId: string, + ): ProjectRecord | undefined; } export interface BranchGateway { listBranchesForProject(projectId: string): RemoteBranchRecord[]; - getBranchForProject(projectId: string, name: string): RemoteBranchRecord | undefined; + getBranchForProject( + projectId: string, + name: string, + ): RemoteBranchRecord | undefined; getDeployment(deploymentId: string): DeploymentRecord | undefined; } @@ -96,13 +113,21 @@ export interface AuthUseCases { listProviders(): Promise; resolveProvider(providerId: string): Promise; listUsersForProvider(providerId: AuthProviderId): Promise; - resolveUserForProvider(providerId: AuthProviderId, userId: string): Promise; + resolveUserForProvider( + providerId: AuthProviderId, + userId: string, + ): Promise; listWorkspacesForUser(userId: string): Promise; - resolveWorkspaceForUser(userId: string, workspaceId: string): Promise; + resolveWorkspaceForUser( + userId: string, + workspaceId: string, + ): Promise; } export interface ProjectUseCases { - list(authState: AuthStateResult): Promise; + list( + authState: AuthStateResult, + ): Promise; listProjectsForWorkspace(workspaceId: string): Promise; } diff --git a/packages/cli/src/use-cases/create-cli-gateways.ts b/packages/cli/src/use-cases/create-cli-gateways.ts index d8f6cdb..c5d76f2 100644 --- a/packages/cli/src/use-cases/create-cli-gateways.ts +++ b/packages/cli/src/use-cases/create-cli-gateways.ts @@ -15,12 +15,15 @@ export interface CliUseCaseGateways { sessionGateway: SessionGateway; } -export function createCliUseCaseGateways(context: CommandContext): CliUseCaseGateways { +export function createCliUseCaseGateways( + context: CommandContext, +): CliUseCaseGateways { return { identityGateway: { listProviders: () => context.api.listProviders(), getProvider: (providerId) => context.api.getProvider(providerId), - listUsersForProvider: (providerId) => context.api.listUsersForProvider(providerId).map(toAuthUser), + listUsersForProvider: (providerId) => + context.api.listUsersForProvider(providerId).map(toAuthUser), getUser: (userId) => { const user = context.api.getUser(userId); return user ? toAuthUser(user) : undefined; @@ -29,7 +32,8 @@ export function createCliUseCaseGateways(context: CommandContext): CliUseCaseGat const user = context.api.getUserForProvider(providerId, userId); return user ? toAuthUser(user) : undefined; }, - listUserWorkspaces: (userId) => context.api.listUserWorkspaces(userId).map(toAuthWorkspace), + listUserWorkspaces: (userId) => + context.api.listUserWorkspaces(userId).map(toAuthWorkspace), getWorkspace: (workspaceId) => { const workspace = context.api.getWorkspace(workspaceId); return workspace ? toAuthWorkspace(workspace) : undefined; @@ -40,9 +44,11 @@ export function createCliUseCaseGateways(context: CommandContext): CliUseCaseGat }, }, projectGateway: { - listProjectsForWorkspace: (workspaceId) => context.api.listProjectsForWorkspace(workspaceId), + listProjectsForWorkspace: (workspaceId) => + context.api.listProjectsForWorkspace(workspaceId), getProject: (projectId) => context.api.getProject(projectId), - getProjectForWorkspace: (workspaceId, projectId) => context.api.getProjectForWorkspace(workspaceId, projectId), + getProjectForWorkspace: (workspaceId, projectId) => + context.api.getProjectForWorkspace(workspaceId, projectId), }, branchGateway: { listBranchesForProject: (projectId) => diff --git a/packages/cli/src/use-cases/project.ts b/packages/cli/src/use-cases/project.ts index e0d4bd8..a576f1d 100644 --- a/packages/cli/src/use-cases/project.ts +++ b/packages/cli/src/use-cases/project.ts @@ -7,18 +7,27 @@ interface ProjectUseCaseDependencies { projectGateway: ProjectGateway; } -export function createProjectUseCases(dependencies: ProjectUseCaseDependencies): ProjectUseCases { +export function createProjectUseCases( + dependencies: ProjectUseCaseDependencies, +): ProjectUseCases { return { list: async (authState: AuthStateResult): Promise => { const workspace = requireWorkspace(authState); return { workspace, - projects: listSortedWorkspaceProjects(dependencies.projectGateway, workspace.id).map(toProjectSummary), + projects: listSortedWorkspaceProjects( + dependencies.projectGateway, + workspace.id, + ).map(toProjectSummary), }; }, - listProjectsForWorkspace: async (workspaceId: string): Promise => - listSortedWorkspaceProjects(dependencies.projectGateway, workspaceId).map(toProjectSummary), + listProjectsForWorkspace: async ( + workspaceId: string, + ): Promise => + listSortedWorkspaceProjects(dependencies.projectGateway, workspaceId).map( + toProjectSummary, + ), }; } @@ -30,14 +39,24 @@ function requireWorkspace(authState: AuthStateResult) { return authState.workspace; } -function listSortedWorkspaceProjects(projectGateway: ProjectGateway, workspaceId: string) { +function listSortedWorkspaceProjects( + projectGateway: ProjectGateway, + workspaceId: string, +) { return projectGateway .listProjectsForWorkspace(workspaceId) .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 toProjectSummary(project: { id: string; name: string; url?: string }): ProjectSummary { +function toProjectSummary(project: { + id: string; + name: string; + url?: string; +}): ProjectSummary { return { id: project.id, name: project.name, diff --git a/packages/cli/tests/app-branch-database.test.ts b/packages/cli/tests/app-branch-database.test.ts index 5457892..3d94fbe 100644 --- a/packages/cli/tests/app-branch-database.test.ts +++ b/packages/cli/tests/app-branch-database.test.ts @@ -4,7 +4,10 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createProjectClient, createResolveBranch } from "./helpers/mock-factories"; +import { + createProjectClient, + createResolveBranch, +} from "./helpers/mock-factories"; beforeEach(() => { process.env.PRISMA_CLI_TEST_REMEMBER_PROJECT_ID = "proj_123"; @@ -52,24 +55,35 @@ describe("app deploy branch database setup", () => { return child; }); vi.doMock("node:child_process", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, spawn, }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); - const { runBranchDatabaseSchemaSetup } = await import("../src/lib/app/branch-database"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); + const { runBranchDatabaseSchemaSetup } = await import( + "../src/lib/app/branch-database" + ); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); - await writeFile(path.join(cwd, "prisma-next.config.ts"), [ - 'import { defineConfig } from "@prisma-next/postgres/config";', - "", - "export default defineConfig({ contract: './src/prisma/contract.prisma' });", - "", - ].join("\n")); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); + await writeFile( + path.join(cwd, "prisma-next.config.ts"), + [ + 'import { defineConfig } from "@prisma-next/postgres/config";', + "", + "export default defineConfig({ contract: './src/prisma/contract.prisma' });", + "", + ].join("\n"), + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -98,7 +112,14 @@ describe("app deploy branch database setup", () => { expect(spawn).toHaveBeenNthCalledWith( 1, "npx", - ["--no-install", "prisma-next", "contract", "emit", "--config", "prisma-next.config.ts"], + [ + "--no-install", + "prisma-next", + "contract", + "emit", + "--config", + "prisma-next.config.ts", + ], expect.objectContaining({ cwd, env: expect.objectContaining({ @@ -111,7 +132,16 @@ describe("app deploy branch database setup", () => { expect(spawn).toHaveBeenNthCalledWith( 2, "npx", - ["--no-install", "prisma-next", "db", "init", "--config", "prisma-next.config.ts", "--db", "postgres://pooled"], + [ + "--no-install", + "prisma-next", + "db", + "init", + "--config", + "prisma-next.config.ts", + "--db", + "postgres://pooled", + ], expect.objectContaining({ cwd, env: expect.objectContaining({ @@ -140,7 +170,14 @@ describe("app deploy branch database setup", () => { expect(spawn).toHaveBeenCalledWith( "npx", - ["--no-install", "prisma", "db", "push", "--schema", "prisma/schema.prisma"], + [ + "--no-install", + "prisma", + "db", + "push", + "--schema", + "prisma/schema.prisma", + ], expect.objectContaining({ cwd, env: expect.objectContaining({ @@ -159,19 +196,29 @@ describe("app deploy branch database setup", () => { return child; }); vi.doMock("node:child_process", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, spawn, }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); - const { runBranchDatabaseSchemaSetup } = await import("../src/lib/app/branch-database"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); + const { runBranchDatabaseSchemaSetup } = await import( + "../src/lib/app/branch-database" + ); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); - await mkdir(path.join(cwd, "node_modules/@prisma/client"), { recursive: true }); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); + await mkdir(path.join(cwd, "node_modules/@prisma/client"), { + recursive: true, + }); await writeFile( path.join(cwd, "node_modules/@prisma/client/package.json"), JSON.stringify({ name: "@prisma/client", version: "5.22.0" }), @@ -203,7 +250,14 @@ describe("app deploy branch database setup", () => { expect(spawn).toHaveBeenCalledWith( "npx", - ["--yes", "prisma@5.22.0", "db", "push", "--schema", "prisma/schema.prisma"], + [ + "--yes", + "prisma@5.22.0", + "db", + "push", + "--schema", + "prisma/schema.prisma", + ], expect.objectContaining({ cwd }), ); }); @@ -215,18 +269,26 @@ describe("app deploy branch database setup", () => { return child; }); vi.doMock("node:child_process", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, spawn, }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); - const { runBranchDatabaseSchemaSetup } = await import("../src/lib/app/branch-database"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); + const { runBranchDatabaseSchemaSetup } = await import( + "../src/lib/app/branch-database" + ); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -254,7 +316,14 @@ describe("app deploy branch database setup", () => { expect(spawn).toHaveBeenCalledWith( "npx", - ["--yes", "prisma@6.19.3", "migrate", "deploy", "--schema", "prisma/schema.prisma"], + [ + "--yes", + "prisma@6.19.3", + "migrate", + "deploy", + "--schema", + "prisma/schema.prisma", + ], expect.objectContaining({ cwd }), ); }); @@ -263,7 +332,13 @@ describe("app deploy branch database setup", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const branchId = "branch_feature_db"; const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const createBranchDatabase = vi.fn().mockResolvedValue({ id: "db_1", @@ -272,13 +347,21 @@ describe("app deploy branch database setup", () => { databaseUrl: "postgres://pooled", directUrl: "postgres://direct", }); - const createEnvironmentVariable = vi.fn().mockImplementation(async (options: { key: string; branchId?: string; className: string }) => ({ - id: `env_${options.key.toLowerCase()}`, - key: options.key, - branchId: options.branchId ?? null, - className: options.className, - isManagedBySystem: false, - })); + const createEnvironmentVariable = vi + .fn() + .mockImplementation( + async (options: { + key: string; + branchId?: string; + className: string; + }) => ({ + id: `env_${options.key.toLowerCase()}`, + key: options.key, + branchId: options.branchId ?? null, + className: options.className, + isManagedBySystem: false, + }), + ); const listEnvironmentVariables = vi.fn().mockResolvedValue([]); const updateEnvironmentVariable = vi.fn(); const deployApp = vi.fn().mockResolvedValue({ @@ -306,7 +389,8 @@ describe("app deploy branch database setup", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/branch-database", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, runBranchDatabaseSchemaSetup, @@ -330,11 +414,16 @@ describe("app deploy branch database setup", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -360,7 +449,9 @@ describe("app deploy branch database setup", () => { branchName: "feature/db", signal: context.runtime.signal, }); - expect(runBranchDatabaseSchemaSetup.mock.calls[0]?.[0].context).toBe(context); + expect(runBranchDatabaseSchemaSetup.mock.calls[0]?.[0].context).toBe( + context, + ); expect(runBranchDatabaseSchemaSetup.mock.calls[0]?.[0]).toEqual( expect.objectContaining({ databaseUrl: "postgres://pooled", @@ -385,8 +476,12 @@ describe("app deploy branch database setup", () => { value: "postgres://direct", }), ); - expect(createBranchDatabase.mock.invocationCallOrder[0]).toBeLessThan(deployApp.mock.invocationCallOrder[0]); - expect(runBranchDatabaseSchemaSetup.mock.invocationCallOrder[0]).toBeLessThan(deployApp.mock.invocationCallOrder[0]); + expect(createBranchDatabase.mock.invocationCallOrder[0]).toBeLessThan( + deployApp.mock.invocationCallOrder[0], + ); + expect( + runBranchDatabaseSchemaSetup.mock.invocationCallOrder[0], + ).toBeLessThan(deployApp.mock.invocationCallOrder[0]); expect(deployApp).toHaveBeenCalledWith( expect.objectContaining({ projectId: "proj_123", @@ -413,10 +508,22 @@ describe("app deploy branch database setup", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const branchId = "branch_main"; const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const listDeployments = vi.fn().mockResolvedValue({ - app: { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + app: { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, deployments: [], }); const createBranchDatabase = vi.fn().mockResolvedValue({ @@ -426,13 +533,21 @@ describe("app deploy branch database setup", () => { databaseUrl: "postgres://pooled", directUrl: "postgres://direct", }); - const createEnvironmentVariable = vi.fn().mockImplementation(async (options: { key: string; branchId?: string; className: string }) => ({ - id: `env_${options.key.toLowerCase()}`, - key: options.key, - branchId: options.branchId ?? null, - className: options.className, - isManagedBySystem: false, - })); + const createEnvironmentVariable = vi + .fn() + .mockImplementation( + async (options: { + key: string; + branchId?: string; + className: string; + }) => ({ + id: `env_${options.key.toLowerCase()}`, + key: options.key, + branchId: options.branchId ?? null, + className: options.className, + isManagedBySystem: false, + }), + ); const deployApp = vi.fn().mockResolvedValue({ projectId: "proj_123", app: { @@ -458,7 +573,8 @@ describe("app deploy branch database setup", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/branch-database", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, runBranchDatabaseSchemaSetup, @@ -482,11 +598,16 @@ describe("app deploy branch database setup", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -533,8 +654,12 @@ describe("app deploy branch database setup", () => { value: "postgres://direct", signal: context.runtime.signal, }); - expect(createBranchDatabase.mock.invocationCallOrder[0]).toBeLessThan(deployApp.mock.invocationCallOrder[0]); - expect(runBranchDatabaseSchemaSetup.mock.invocationCallOrder[0]).toBeLessThan(deployApp.mock.invocationCallOrder[0]); + expect(createBranchDatabase.mock.invocationCallOrder[0]).toBeLessThan( + deployApp.mock.invocationCallOrder[0], + ); + expect( + runBranchDatabaseSchemaSetup.mock.invocationCallOrder[0], + ).toBeLessThan(deployApp.mock.invocationCallOrder[0]); expect(result.result.branchDatabase).toEqual({ status: "created", database: { @@ -554,7 +679,13 @@ describe("app deploy branch database setup", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const branchId = "branch_feature_next"; const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const createBranchDatabase = vi.fn().mockResolvedValue({ id: "db_1", @@ -563,13 +694,21 @@ describe("app deploy branch database setup", () => { databaseUrl: "postgres://pooled", directUrl: "postgres://direct", }); - const createEnvironmentVariable = vi.fn().mockImplementation(async (options: { key: string; branchId?: string; className: string }) => ({ - id: `env_${options.key.toLowerCase()}`, - key: options.key, - branchId: options.branchId ?? null, - className: options.className, - isManagedBySystem: false, - })); + const createEnvironmentVariable = vi + .fn() + .mockImplementation( + async (options: { + key: string; + branchId?: string; + className: string; + }) => ({ + id: `env_${options.key.toLowerCase()}`, + key: options.key, + branchId: options.branchId ?? null, + className: options.className, + isManagedBySystem: false, + }), + ); const runBranchDatabaseSchemaSetup = vi.fn().mockResolvedValue({ command: "prisma-next-db-init", source: "prisma-next", @@ -595,7 +734,8 @@ describe("app deploy branch database setup", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/branch-database", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, runBranchDatabaseSchemaSetup, @@ -619,17 +759,22 @@ describe("app deploy branch database setup", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); - await writeFile(path.join(cwd, "prisma-next.config.ts"), [ - 'import { defineConfig } from "@prisma-next/postgres/config";', - "", - "export default defineConfig({", - " db: { connection: process.env.DATABASE_URL! },", - "});", - "", - ].join("\n")); + await writeFile( + path.join(cwd, "prisma-next.config.ts"), + [ + 'import { defineConfig } from "@prisma-next/postgres/config";', + "", + "export default defineConfig({", + " db: { connection: process.env.DATABASE_URL! },", + "});", + "", + ].join("\n"), + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -668,7 +813,9 @@ describe("app deploy branch database setup", () => { value: "postgres://pooled", }), ); - expect(runBranchDatabaseSchemaSetup.mock.invocationCallOrder[0]).toBeLessThan(deployApp.mock.invocationCallOrder[0]); + expect( + runBranchDatabaseSchemaSetup.mock.invocationCallOrder[0], + ).toBeLessThan(deployApp.mock.invocationCallOrder[0]); expect(result.result.branchDatabase).toEqual({ status: "created", database: { @@ -688,7 +835,13 @@ describe("app deploy branch database setup", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const branchId = "branch_feature_db"; const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const createBranchDatabase = vi.fn(); const createEnvironmentVariable = vi.fn(); @@ -708,18 +861,22 @@ describe("app deploy branch database setup", () => { url: "https://hello-world.prisma.app", }, }); - const listEnvironmentVariables = vi.fn().mockImplementation(async (options: { key?: string }) => { - if (options.key === "DATABASE_URL") { - return [{ - id: "env_database_url", - key: "DATABASE_URL", - branchId, - className: "preview", - isManagedBySystem: false, - }]; - } - return []; - }); + const listEnvironmentVariables = vi + .fn() + .mockImplementation(async (options: { key?: string }) => { + if (options.key === "DATABASE_URL") { + return [ + { + id: "env_database_url", + key: "DATABASE_URL", + branchId, + className: "preview", + isManagedBySystem: false, + }, + ]; + } + return []; + }); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, @@ -743,11 +900,16 @@ describe("app deploy branch database setup", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -784,7 +946,13 @@ describe("app deploy branch database setup", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const branchId = "branch_main"; const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const createBranchDatabase = vi.fn(); const createEnvironmentVariable = vi.fn(); @@ -803,30 +971,38 @@ describe("app deploy branch database setup", () => { url: "https://hello-world.prisma.app", }, }); - const listEnvironmentVariables = vi.fn().mockImplementation(async (options: { key?: string; className?: string }) => { - if (options.className !== "production") { - return []; - } - if (options.key === "DATABASE_URL") { - return [{ - id: "env_database_url", - key: "DATABASE_URL", - branchId: null, - className: "production", - isManagedBySystem: false, - }]; - } - if (options.key === "DIRECT_URL") { - return [{ - id: "env_direct_url", - key: "DIRECT_URL", - branchId: null, - className: "production", - isManagedBySystem: false, - }]; - } - return []; - }); + const listEnvironmentVariables = vi + .fn() + .mockImplementation( + async (options: { key?: string; className?: string }) => { + if (options.className !== "production") { + return []; + } + if (options.key === "DATABASE_URL") { + return [ + { + id: "env_database_url", + key: "DATABASE_URL", + branchId: null, + className: "production", + isManagedBySystem: false, + }, + ]; + } + if (options.key === "DIRECT_URL") { + return [ + { + id: "env_direct_url", + key: "DIRECT_URL", + branchId: null, + className: "production", + isManagedBySystem: false, + }, + ]; + } + return []; + }, + ); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, @@ -845,14 +1021,22 @@ describe("app deploy branch database setup", () => { updateEnvironmentVariable, deployApp, listDeployments: vi.fn().mockResolvedValue({ - app: { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + app: { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, deployments: [], }), showDeployment: vi.fn(), })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const { context } = await createTestCommandContext({ @@ -895,11 +1079,20 @@ describe("app deploy branch database setup", () => { existingKey: "DIRECT_URL", envVarId: "env_direct_url", }, - ] as const)("deploy --db treats an existing production $existingKey as BYO DB and leaves it unchanged", async ({ existingKey, envVarId }) => { + ] as const)("deploy --db treats an existing production $existingKey as BYO DB and leaves it unchanged", async ({ + existingKey, + envVarId, + }) => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const branchId = "branch_main"; const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const createBranchDatabase = vi.fn(); const createEnvironmentVariable = vi.fn(); @@ -918,19 +1111,28 @@ describe("app deploy branch database setup", () => { url: "https://hello-world.prisma.app", }, }); - const listEnvironmentVariables = vi.fn().mockImplementation(async (options: { key?: string; className?: string }) => { - if (options.className !== "production" || options.key !== existingKey) { - return []; - } - - return [{ - id: envVarId, - key: existingKey, - branchId: null, - className: "production", - isManagedBySystem: false, - }]; - }); + const listEnvironmentVariables = vi + .fn() + .mockImplementation( + async (options: { key?: string; className?: string }) => { + if ( + options.className !== "production" || + options.key !== existingKey + ) { + return []; + } + + return [ + { + id: envVarId, + key: existingKey, + branchId: null, + className: "production", + isManagedBySystem: false, + }, + ]; + }, + ); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, @@ -949,14 +1151,22 @@ describe("app deploy branch database setup", () => { updateEnvironmentVariable, deployApp, listDeployments: vi.fn().mockResolvedValue({ - app: { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + app: { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, deployments: [], }), showDeployment: vi.fn(), })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const { context } = await createTestCommandContext({ @@ -994,7 +1204,13 @@ describe("app deploy branch database setup", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const branchId = "branch_feature_db"; const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const createBranchDatabase = vi.fn().mockResolvedValue({ id: "db_1", @@ -1031,24 +1247,29 @@ describe("app deploy branch database setup", () => { url: "https://hello-world.prisma.app", }, }); - const listEnvironmentVariables = vi.fn().mockImplementation(async (options: { key?: string }) => { - if (options.key === "DIRECT_URL") { - return [{ - id: "env_direct_url", - key: "DIRECT_URL", - branchId, - className: "preview", - isManagedBySystem: false, - }]; - } - return []; - }); + const listEnvironmentVariables = vi + .fn() + .mockImplementation(async (options: { key?: string }) => { + if (options.key === "DIRECT_URL") { + return [ + { + id: "env_direct_url", + key: "DIRECT_URL", + branchId, + className: "preview", + isManagedBySystem: false, + }, + ]; + } + return []; + }); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/branch-database", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, runBranchDatabaseSchemaSetup: vi.fn().mockResolvedValue({ @@ -1076,11 +1297,16 @@ describe("app deploy branch database setup", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -1122,7 +1348,13 @@ describe("app deploy branch database setup", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const branchId = "branch_feature_db"; const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const createBranchDatabase = vi.fn().mockResolvedValue({ id: "db_1", @@ -1154,24 +1386,29 @@ describe("app deploy branch database setup", () => { url: "https://hello-world.prisma.app", }, }); - const listEnvironmentVariables = vi.fn().mockImplementation(async (options: { key?: string }) => { - if (options.key === "DIRECT_URL") { - return [{ - id: "env_direct_url", - key: "DIRECT_URL", - branchId, - className: "preview", - isManagedBySystem: false, - }]; - } - return []; - }); + const listEnvironmentVariables = vi + .fn() + .mockImplementation(async (options: { key?: string }) => { + if (options.key === "DIRECT_URL") { + return [ + { + id: "env_direct_url", + key: "DIRECT_URL", + branchId, + className: "preview", + isManagedBySystem: false, + }, + ]; + } + return []; + }); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/branch-database", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, runBranchDatabaseSchemaSetup: vi.fn().mockResolvedValue({ @@ -1200,11 +1437,16 @@ describe("app deploy branch database setup", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -1246,7 +1488,13 @@ describe("app deploy branch database setup", () => { const branchId = "branch_feature_db"; const confirmPrompt = vi.fn().mockResolvedValue(true); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const createBranchDatabase = vi.fn().mockResolvedValue({ id: "db_1", @@ -1282,14 +1530,17 @@ describe("app deploy branch database setup", () => { requireComputeAuth, })); vi.doMock("../src/shell/prompt", async () => { - const actual = await vi.importActual("../src/shell/prompt"); + const actual = await vi.importActual< + typeof import("../src/shell/prompt") + >("../src/shell/prompt"); return { ...actual, confirmPrompt, }; }); vi.doMock("../src/lib/app/branch-database", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, runBranchDatabaseSchemaSetup: vi.fn().mockResolvedValue({ @@ -1317,11 +1568,16 @@ describe("app deploy branch database setup", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -1377,7 +1633,13 @@ describe("app deploy branch database setup", () => { role: "production", }), listApps: vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]), createBranchDatabase, listEnvironmentVariables: vi.fn().mockResolvedValue([]), @@ -1385,18 +1647,29 @@ describe("app deploy branch database setup", () => { updateEnvironmentVariable: vi.fn(), deployApp, listDeployments: vi.fn().mockResolvedValue({ - app: { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + app: { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, deployments: [], }), showDeployment: vi.fn(), })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -1437,7 +1710,13 @@ describe("app deploy branch database setup", () => { role: "production", }), listApps: vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_live", liveUrl: "https://hello-world.prisma.app" }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_live", + liveUrl: "https://hello-world.prisma.app", + }, ]), createBranchDatabase, listEnvironmentVariables: vi.fn().mockResolvedValue([]), @@ -1445,20 +1724,30 @@ describe("app deploy branch database setup", () => { updateEnvironmentVariable: vi.fn(), deployApp, listDeployments: vi.fn().mockResolvedValue({ - app: { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_live", liveUrl: "https://hello-world.prisma.app" }, - deployments: [{ - id: "dep_live", - status: "running", - createdAt: "2026-06-01T00:00:00.000Z", - url: "https://hello-world.prisma.app", - live: true, - }], + app: { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_live", + liveUrl: "https://hello-world.prisma.app", + }, + deployments: [ + { + id: "dep_live", + status: "running", + createdAt: "2026-06-01T00:00:00.000Z", + url: "https://hello-world.prisma.app", + live: true, + }, + ], }), showDeployment: vi.fn(), })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const { context } = await createTestCommandContext({ @@ -1473,16 +1762,19 @@ describe("app deploy branch database setup", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - projectRef: "proj_123", - branchName: "main", - framework: "hono", - prod: true, - db: true, - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + branchName: "main", + framework: "hono", + prod: true, + db: true, + }), + ).rejects.toMatchObject({ code: "USAGE_ERROR", domain: "app", - summary: "Database setup is only available during the first production deploy", + summary: + "Database setup is only available during the first production deploy", }); expect(createBranchDatabase).not.toHaveBeenCalled(); expect(deployApp).not.toHaveBeenCalled(); @@ -1500,7 +1792,13 @@ describe("app deploy branch database setup", () => { createPreviewAppProvider: vi.fn(() => ({ resolveBranch: createResolveBranch(), listApps: vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]), createBranchDatabase, listEnvironmentVariables: vi.fn().mockResolvedValue([]), @@ -1512,10 +1810,15 @@ describe("app deploy branch database setup", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); - await writeFile(path.join(cwd, ".env"), "DATABASE_URL=postgresql://example\n"); + await writeFile( + path.join(cwd, ".env"), + "DATABASE_URL=postgresql://example\n", + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -1528,16 +1831,19 @@ describe("app deploy branch database setup", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - projectRef: "proj_123", - branchName: "feature/db", - framework: "hono", - envAssignments: [".env"], - db: true, - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + branchName: "feature/db", + framework: "hono", + envAssignments: [".env"], + db: true, + }), + ).rejects.toMatchObject({ code: "USAGE_ERROR", domain: "app", - summary: "Database setup cannot be combined with provided database env vars", + summary: + "Database setup cannot be combined with provided database env vars", }); expect(createBranchDatabase).not.toHaveBeenCalled(); expect(deployApp).not.toHaveBeenCalled(); @@ -1547,7 +1853,13 @@ describe("app deploy branch database setup", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const branchId = "branch_feature_db"; const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const createBranchDatabase = vi.fn().mockResolvedValue({ id: "db_1", @@ -1560,13 +1872,16 @@ describe("app deploy branch database setup", () => { const createEnvironmentVariable = vi.fn(); const updateEnvironmentVariable = vi.fn(); const deployApp = vi.fn(); - const runBranchDatabaseSchemaSetup = vi.fn().mockRejectedValue(new Error("Migration failed")); + const runBranchDatabaseSchemaSetup = vi + .fn() + .mockRejectedValue(new Error("Migration failed")); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/branch-database", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, runBranchDatabaseSchemaSetup, @@ -1591,11 +1906,16 @@ describe("app deploy branch database setup", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -1608,12 +1928,14 @@ describe("app deploy branch database setup", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - projectRef: "proj_123", - branchName: "feature/db", - framework: "hono", - db: true, - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + branchName: "feature/db", + framework: "hono", + db: true, + }), + ).rejects.toMatchObject({ code: "SCHEMA_SETUP_FAILED", domain: "app", }); @@ -1631,7 +1953,13 @@ describe("app deploy branch database setup", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const branchId = "branch_feature_db"; const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const createBranchDatabase = vi.fn().mockResolvedValue({ id: "db_1", @@ -1641,14 +1969,17 @@ describe("app deploy branch database setup", () => { directUrl: null, }); const deleteBranchDatabase = vi.fn().mockResolvedValue(undefined); - const createEnvironmentVariable = vi.fn().mockRejectedValue(new Error("env write failed")); + const createEnvironmentVariable = vi + .fn() + .mockRejectedValue(new Error("env write failed")); const deployApp = vi.fn(); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/branch-database", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal(); return { ...actual, runBranchDatabaseSchemaSetup: vi.fn().mockResolvedValue({ @@ -1677,11 +2008,16 @@ describe("app deploy branch database setup", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -1694,12 +2030,14 @@ describe("app deploy branch database setup", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - projectRef: "proj_123", - branchName: "feature/db", - framework: "hono", - db: true, - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + branchName: "feature/db", + framework: "hono", + db: true, + }), + ).rejects.toMatchObject({ code: "BRANCH_DATABASE_SETUP_FAILED", domain: "app", }); @@ -1712,34 +2050,50 @@ describe("app deploy branch database setup", () => { it("chooses a deterministic schema.prisma when multiple schemas exist", async () => { const { createTempCwd } = await import("./helpers"); - const { inspectBranchDatabaseSignal } = await import("../src/lib/app/branch-database"); + const { inspectBranchDatabaseSignal } = await import( + "../src/lib/app/branch-database" + ); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "packages/a/prisma"), { recursive: true }); await mkdir(path.join(cwd, "prisma"), { recursive: true }); await writeFile(path.join(cwd, "packages/a/prisma/schema.prisma"), ""); await writeFile(path.join(cwd, "prisma/schema.prisma"), ""); - const signal = await inspectBranchDatabaseSignal(cwd, new AbortController().signal); + const signal = await inspectBranchDatabaseSignal( + cwd, + new AbortController().signal, + ); expect(signal.schema?.path).toBe(path.join(cwd, "prisma/schema.prisma")); }); it("prefers a Prisma Next config over schema.prisma when both exist", async () => { const { createTempCwd } = await import("./helpers"); - const { inspectBranchDatabaseSignal } = await import("../src/lib/app/branch-database"); + const { inspectBranchDatabaseSignal } = await import( + "../src/lib/app/branch-database" + ); const cwd = await createTempCwd(); await mkdir(path.join(cwd, "prisma"), { recursive: true }); - await writeFile(path.join(cwd, "prisma/schema.prisma"), "datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n"); - await writeFile(path.join(cwd, "prisma-next.config.ts"), [ - 'import { defineConfig } from "@prisma-next/postgres/config";', - "", - "export default defineConfig({", - " db: { connection: process.env.DATABASE_URL! },", - "});", - "", - ].join("\n")); - - const signal = await inspectBranchDatabaseSignal(cwd, new AbortController().signal); + await writeFile( + path.join(cwd, "prisma/schema.prisma"), + 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }\n', + ); + await writeFile( + path.join(cwd, "prisma-next.config.ts"), + [ + 'import { defineConfig } from "@prisma-next/postgres/config";', + "", + "export default defineConfig({", + " db: { connection: process.env.DATABASE_URL! },", + "});", + "", + ].join("\n"), + ); + + const signal = await inspectBranchDatabaseSignal( + cwd, + new AbortController().signal, + ); expect(signal.schema).toMatchObject({ kind: "prisma-next", @@ -1752,18 +2106,25 @@ describe("app deploy branch database setup", () => { it("treats non-Postgres Prisma Next configs as unsupported branch database signals", async () => { const { createTempCwd } = await import("./helpers"); - const { hasBranchDatabaseSignal, inspectBranchDatabaseSignal } = await import("../src/lib/app/branch-database"); + const { hasBranchDatabaseSignal, inspectBranchDatabaseSignal } = + await import("../src/lib/app/branch-database"); const cwd = await createTempCwd(); - await writeFile(path.join(cwd, "prisma-next.config.ts"), [ - 'import { defineConfig } from "@prisma-next/mongo/config";', - "", - "export default defineConfig({", - " db: { connection: process.env.DATABASE_URL! },", - "});", - "", - ].join("\n")); + await writeFile( + path.join(cwd, "prisma-next.config.ts"), + [ + 'import { defineConfig } from "@prisma-next/mongo/config";', + "", + "export default defineConfig({", + " db: { connection: process.env.DATABASE_URL! },", + "});", + "", + ].join("\n"), + ); - const signal = await inspectBranchDatabaseSignal(cwd, new AbortController().signal); + const signal = await inspectBranchDatabaseSignal( + cwd, + new AbortController().signal, + ); expect(signal.schema).toBeNull(); expect(signal.unsupportedSchema).toMatchObject({ @@ -1786,7 +2147,13 @@ describe("app deploy branch database setup", () => { createPreviewAppProvider: vi.fn(() => ({ resolveBranch: createResolveBranch(), listApps: vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]), createBranchDatabase, listEnvironmentVariables: vi.fn().mockResolvedValue([]), @@ -1798,17 +2165,22 @@ describe("app deploy branch database setup", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); - await writeFile(path.join(cwd, "prisma-next.config.ts"), [ - 'import { defineConfig } from "@prisma-next/sqlite/config";', - "", - "export default defineConfig({", - " db: { connection: process.env.DATABASE_URL! },", - "});", - "", - ].join("\n")); + await writeFile( + path.join(cwd, "prisma-next.config.ts"), + [ + 'import { defineConfig } from "@prisma-next/sqlite/config";', + "", + "export default defineConfig({", + " db: { connection: process.env.DATABASE_URL! },", + "});", + "", + ].join("\n"), + ); const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state"), @@ -1821,12 +2193,14 @@ describe("app deploy branch database setup", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - projectRef: "proj_123", - branchName: "feature/db", - framework: "hono", - db: true, - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + branchName: "feature/db", + framework: "hono", + db: true, + }), + ).rejects.toMatchObject({ code: "USAGE_ERROR", domain: "app", summary: "Database setup is not available for this Prisma schema", diff --git a/packages/cli/tests/app-build.test.ts b/packages/cli/tests/app-build.test.ts index c6b7d8c..f2dfc5f 100644 --- a/packages/cli/tests/app-build.test.ts +++ b/packages/cli/tests/app-build.test.ts @@ -1,4 +1,12 @@ -import { access, lstat, mkdir, readFile, readlink, symlink, writeFile } from "node:fs/promises"; +import { + access, + lstat, + mkdir, + readFile, + readlink, + symlink, + writeFile, +} from "node:fs/promises"; import { createRequire } from "node:module"; import path from "node:path"; @@ -13,22 +21,29 @@ afterEach(() => { describe("preview build strategy", () => { it("creates prisma.app.json with inferred Next.js settings", async () => { - const { PRISMA_APP_CONFIG_SCHEMA_URL, resolveOrCreatePreviewBuildSettings } = await import("../src/lib/app/preview-build"); + const { + PRISMA_APP_CONFIG_SCHEMA_URL, + resolveOrCreatePreviewBuildSettings, + } = await import("../src/lib/app/preview-build"); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); await mkdir(appPath, { recursive: true }); await writeFile( path.join(appPath, "package.json"), - JSON.stringify({ - packageManager: "bun@1.2.0", - scripts: { - build: "prisma generate && next build", - }, - dependencies: { - next: "15.0.0", + JSON.stringify( + { + packageManager: "bun@1.2.0", + scripts: { + build: "prisma generate && next build", + }, + dependencies: { + next: "15.0.0", + }, }, - }, null, 2), + null, + 2, + ), "utf8", ); @@ -45,15 +60,25 @@ describe("preview build strategy", () => { outputDirectory: ".next/standalone", outputDirectorySource: "Next.js output", }); - await expect(readFile(path.join(appPath, "prisma.app.json"), "utf8")).resolves.toBe(`${JSON.stringify({ - $schema: PRISMA_APP_CONFIG_SCHEMA_URL, - buildCommand: "bun run build", - outputDirectory: ".next/standalone", - }, null, 2)}\n`); + await expect( + readFile(path.join(appPath, "prisma.app.json"), "utf8"), + ).resolves.toBe( + `${JSON.stringify( + { + $schema: PRISMA_APP_CONFIG_SCHEMA_URL, + buildCommand: "bun run build", + outputDirectory: ".next/standalone", + }, + null, + 2, + )}\n`, + ); }); it("packages the full tree with a next start launcher when the build produces no standalone output", async () => { - const { PreviewBuildStrategy } = await import("../src/lib/app/preview-build"); + const { PreviewBuildStrategy } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); @@ -64,16 +89,31 @@ describe("preview build strategy", () => { JSON.stringify({ dependencies: { next: "15.0.0" } }), "utf8", ); - await writeFile(path.join(appPath, ".next/BUILD_ID"), "fallback-test", "utf8"); - await writeFile(path.join(appPath, ".env"), "SECRET=should-not-ship", "utf8"); - await writeFile(path.join(appPath, ".env.local"), "SECRET=should-not-ship", "utf8"); + await writeFile( + path.join(appPath, ".next/BUILD_ID"), + "fallback-test", + "utf8", + ); + await writeFile( + path.join(appPath, ".env"), + "SECRET=should-not-ship", + "utf8", + ); + await writeFile( + path.join(appPath, ".env.local"), + "SECRET=should-not-ship", + "utf8", + ); await writeFile( path.join(appPath, "node_modules/next/package.json"), JSON.stringify({ name: "next", version: "15.0.0" }), "utf8", ); await mkdir(path.join(appPath, "node_modules/.bin"), { recursive: true }); - await symlink("../next/package.json", path.join(appPath, "node_modules/.bin/next-link")); + await symlink( + "../next/package.json", + path.join(appPath, "node_modules/.bin/next-link"), + ); const strategy = new PreviewBuildStrategy({ appPath, @@ -91,20 +131,37 @@ describe("preview build strategy", () => { expect(artifact.entrypoint).toBe("prisma-next-start.cjs"); expect(artifact.defaultPortMapping).toEqual({ http: 3000 }); - const launcher = await readFile(path.join(artifact.directory, "prisma-next-start.cjs"), "utf8"); + const launcher = await readFile( + path.join(artifact.directory, "prisma-next-start.cjs"), + "utf8", + ); expect(launcher).toContain('require("next/dist/bin/next")'); expect(launcher).toContain('process.argv.push("start"'); expect(launcher).toContain("process.chdir(__dirname)"); - await expect(readFile(path.join(artifact.directory, ".next/BUILD_ID"), "utf8")).resolves.toBe("fallback-test"); - await expect(readFile(path.join(artifact.directory, "node_modules/next/package.json"), "utf8")).resolves.toContain("15.0.0"); - - const linkPath = path.join(artifact.directory, "node_modules/.bin/next-link"); + await expect( + readFile(path.join(artifact.directory, ".next/BUILD_ID"), "utf8"), + ).resolves.toBe("fallback-test"); + await expect( + readFile( + path.join(artifact.directory, "node_modules/next/package.json"), + "utf8", + ), + ).resolves.toContain("15.0.0"); + + const linkPath = path.join( + artifact.directory, + "node_modules/.bin/next-link", + ); expect((await lstat(linkPath)).isSymbolicLink()).toBe(true); await expect(readlink(linkPath)).resolves.toBe("../next/package.json"); - await expect(access(path.join(artifact.directory, ".env"))).rejects.toThrow(); - await expect(access(path.join(artifact.directory, ".env.local"))).rejects.toThrow(); + await expect( + access(path.join(artifact.directory, ".env")), + ).rejects.toThrow(); + await expect( + access(path.join(artifact.directory, ".env.local")), + ).rejects.toThrow(); } finally { const stagedDir = artifact.directory; await artifact.cleanup?.(); @@ -113,7 +170,9 @@ describe("preview build strategy", () => { }); it("creates TanStack and Hono build config defaults", async () => { - const { resolveOrCreatePreviewBuildSettings } = await import("../src/lib/app/preview-build"); + const { resolveOrCreatePreviewBuildSettings } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const tanstackPath = path.join(cwd, "tanstack"); const honoPath = path.join(cwd, "hono"); @@ -121,38 +180,50 @@ describe("preview build strategy", () => { await mkdir(tanstackPath, { recursive: true }); await writeFile( path.join(tanstackPath, "package.json"), - JSON.stringify({ - dependencies: { - "@tanstack/react-start": "1.0.0", + JSON.stringify( + { + dependencies: { + "@tanstack/react-start": "1.0.0", + }, }, - }, null, 2), + null, + 2, + ), "utf8", ); await mkdir(honoPath, { recursive: true }); await writeFile( path.join(honoPath, "package.json"), - JSON.stringify({ - dependencies: { - hono: "4.0.0", + JSON.stringify( + { + dependencies: { + hono: "4.0.0", + }, }, - }, null, 2), + null, + 2, + ), "utf8", ); - await expect(resolveOrCreatePreviewBuildSettings({ - appPath: tanstackPath, - buildType: "tanstack-start", - })).resolves.toMatchObject({ + await expect( + resolveOrCreatePreviewBuildSettings({ + appPath: tanstackPath, + buildType: "tanstack-start", + }), + ).resolves.toMatchObject({ status: "created", settings: { buildCommand: "vite build", outputDirectory: ".output", }, }); - await expect(resolveOrCreatePreviewBuildSettings({ - appPath: honoPath, - buildType: "bun", - })).resolves.toMatchObject({ + await expect( + resolveOrCreatePreviewBuildSettings({ + appPath: honoPath, + buildType: "bun", + }), + ).resolves.toMatchObject({ status: "created", settings: { buildCommand: null, @@ -162,7 +233,9 @@ describe("preview build strategy", () => { }); it("uses an existing prisma.app.json without overwriting it", async () => { - const { resolveOrCreatePreviewBuildSettings } = await import("../src/lib/app/preview-build"); + const { resolveOrCreatePreviewBuildSettings } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); const configPath = path.join(appPath, "prisma.app.json"); @@ -175,22 +248,28 @@ describe("preview build strategy", () => { await mkdir(appPath, { recursive: true }); await writeFile( path.join(appPath, "package.json"), - JSON.stringify({ - scripts: { - build: "next build", - }, - dependencies: { - next: "15.0.0", + JSON.stringify( + { + scripts: { + build: "next build", + }, + dependencies: { + next: "15.0.0", + }, }, - }, null, 2), + null, + 2, + ), "utf8", ); await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); - await expect(resolveOrCreatePreviewBuildSettings({ - appPath, - buildType: "nextjs", - })).resolves.toMatchObject({ + await expect( + resolveOrCreatePreviewBuildSettings({ + appPath, + buildType: "nextjs", + }), + ).resolves.toMatchObject({ status: "used", settings: { buildCommand: null, @@ -199,60 +278,82 @@ describe("preview build strategy", () => { outputDirectorySource: null, }, }); - await expect(readFile(configPath, "utf8")).resolves.toBe(`${JSON.stringify(config, null, 2)}\n`); + await expect(readFile(configPath, "utf8")).resolves.toBe( + `${JSON.stringify(config, null, 2)}\n`, + ); }); it("rejects invalid prisma.app.json files", async () => { - const { resolveOrCreatePreviewBuildSettings } = await import("../src/lib/app/preview-build"); + const { resolveOrCreatePreviewBuildSettings } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const invalidJsonPath = path.join(cwd, "invalid-json"); const escapingPath = path.join(cwd, "escaping-output"); await mkdir(invalidJsonPath, { recursive: true }); - await writeFile(path.join(invalidJsonPath, "prisma.app.json"), "{ nope\n", "utf8"); + await writeFile( + path.join(invalidJsonPath, "prisma.app.json"), + "{ nope\n", + "utf8", + ); await mkdir(escapingPath, { recursive: true }); await writeFile( path.join(escapingPath, "prisma.app.json"), - JSON.stringify({ - buildCommand: "bun run build", - outputDirectory: "../dist", - }, null, 2), + JSON.stringify( + { + buildCommand: "bun run build", + outputDirectory: "../dist", + }, + null, + 2, + ), "utf8", ); - await expect(resolveOrCreatePreviewBuildSettings({ - appPath: invalidJsonPath, - buildType: "nextjs", - })).rejects.toMatchObject({ + await expect( + resolveOrCreatePreviewBuildSettings({ + appPath: invalidJsonPath, + buildType: "nextjs", + }), + ).rejects.toMatchObject({ code: "APP_CONFIG_INVALID", domain: "app", }); - await expect(resolveOrCreatePreviewBuildSettings({ - appPath: escapingPath, - buildType: "nextjs", - })).rejects.toMatchObject({ + await expect( + resolveOrCreatePreviewBuildSettings({ + appPath: escapingPath, + buildType: "nextjs", + }), + ).rejects.toMatchObject({ code: "APP_CONFIG_INVALID", domain: "app", }); }); it("resolves package.json build scripts and literal framework output directories", async () => { - const { resolvePreviewBuildSettings } = await import("../src/lib/app/preview-build"); + const { resolvePreviewBuildSettings } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); await mkdir(appPath, { recursive: true }); await writeFile( path.join(appPath, "package.json"), - JSON.stringify({ - packageManager: "pnpm@10.0.0", - scripts: { - build: "prisma generate && next build", - }, - dependencies: { - next: "15.0.0", + JSON.stringify( + { + packageManager: "pnpm@10.0.0", + scripts: { + build: "prisma generate && next build", + }, + dependencies: { + next: "15.0.0", + }, }, - }, null, 2), + null, + 2, + ), "utf8", ); await writeFile( @@ -261,10 +362,12 @@ describe("preview build strategy", () => { "utf8", ); - await expect(resolvePreviewBuildSettings({ - appPath, - buildType: "nextjs", - })).resolves.toEqual({ + await expect( + resolvePreviewBuildSettings({ + appPath, + buildType: "nextjs", + }), + ).resolves.toEqual({ buildCommand: "pnpm run build", buildCommandSource: "package.json scripts.build", outputDirectory: "build/standalone", @@ -273,22 +376,28 @@ describe("preview build strategy", () => { }); it("only reads Next.js distDir from the exported config object", async () => { - const { resolvePreviewBuildSettings } = await import("../src/lib/app/preview-build"); + const { resolvePreviewBuildSettings } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); await mkdir(appPath, { recursive: true }); await writeFile( path.join(appPath, "package.json"), - JSON.stringify({ - packageManager: "pnpm@10.0.0", - scripts: { - build: "next build", - }, - dependencies: { - next: "15.0.0", + JSON.stringify( + { + packageManager: "pnpm@10.0.0", + scripts: { + build: "next build", + }, + dependencies: { + next: "15.0.0", + }, }, - }, null, 2), + null, + 2, + ), "utf8", ); await writeFile( @@ -301,32 +410,40 @@ describe("preview build strategy", () => { "utf8", ); - await expect(resolvePreviewBuildSettings({ - appPath, - buildType: "nextjs", - })).resolves.toMatchObject({ + await expect( + resolvePreviewBuildSettings({ + appPath, + buildType: "nextjs", + }), + ).resolves.toMatchObject({ outputDirectory: "build/standalone", outputDirectorySource: "next.config distDir", }); }); it("ignores commented or unrelated Next.js distDir values", async () => { - const { resolvePreviewBuildSettings } = await import("../src/lib/app/preview-build"); + const { resolvePreviewBuildSettings } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); await mkdir(appPath, { recursive: true }); await writeFile( path.join(appPath, "package.json"), - JSON.stringify({ - packageManager: "pnpm@10.0.0", - scripts: { - build: "next build", - }, - dependencies: { - next: "15.0.0", + JSON.stringify( + { + packageManager: "pnpm@10.0.0", + scripts: { + build: "next build", + }, + dependencies: { + next: "15.0.0", + }, }, - }, null, 2), + null, + 2, + ), "utf8", ); await writeFile( @@ -339,17 +456,21 @@ describe("preview build strategy", () => { "utf8", ); - await expect(resolvePreviewBuildSettings({ - appPath, - buildType: "nextjs", - })).resolves.toMatchObject({ + await expect( + resolvePreviewBuildSettings({ + appPath, + buildType: "nextjs", + }), + ).resolves.toMatchObject({ outputDirectory: ".next/standalone", outputDirectorySource: "Next.js output", }); }); it("detects the package manager for package.json build scripts", async () => { - const { resolvePreviewBuildSettings } = await import("../src/lib/app/preview-build"); + const { resolvePreviewBuildSettings } = await import( + "../src/lib/app/preview-build" + ); const cases = [ { lockfile: "bun.lock", command: "bun run build" }, { lockfile: "pnpm-lock.yaml", command: "pnpm run build" }, @@ -364,22 +485,28 @@ describe("preview build strategy", () => { await mkdir(appPath, { recursive: true }); await writeFile( path.join(appPath, "package.json"), - JSON.stringify({ - scripts: { - build: "next build", - }, - dependencies: { - next: "15.0.0", + JSON.stringify( + { + scripts: { + build: "next build", + }, + dependencies: { + next: "15.0.0", + }, }, - }, null, 2), + null, + 2, + ), "utf8", ); await writeFile(path.join(appPath, testCase.lockfile), "", "utf8"); - await expect(resolvePreviewBuildSettings({ - appPath, - buildType: "nextjs", - })).resolves.toMatchObject({ + await expect( + resolvePreviewBuildSettings({ + appPath, + buildType: "nextjs", + }), + ).resolves.toMatchObject({ buildCommand: testCase.command, buildCommandSource: "package.json scripts.build", }); @@ -387,35 +514,45 @@ describe("preview build strategy", () => { }); it("uses the literal package.json build script when no package manager is detected", async () => { - const { resolvePreviewBuildSettings } = await import("../src/lib/app/preview-build"); + const { resolvePreviewBuildSettings } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); await mkdir(appPath, { recursive: true }); await writeFile( path.join(appPath, "package.json"), - JSON.stringify({ - scripts: { - build: "custom-build", - }, - dependencies: { - next: "15.0.0", + JSON.stringify( + { + scripts: { + build: "custom-build", + }, + dependencies: { + next: "15.0.0", + }, }, - }, null, 2), + null, + 2, + ), "utf8", ); - await expect(resolvePreviewBuildSettings({ - appPath, - buildType: "nextjs", - })).resolves.toMatchObject({ + await expect( + resolvePreviewBuildSettings({ + appPath, + buildType: "nextjs", + }), + ).resolves.toMatchObject({ buildCommand: "custom-build", buildCommandSource: "package.json scripts.build", }); }); it("does not detect unsupported next.config.cjs files as Next.js", async () => { - const { resolvePreviewBuildStrategy } = await import("../src/lib/app/preview-build"); + const { resolvePreviewBuildStrategy } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); @@ -425,14 +562,24 @@ describe("preview build strategy", () => { `${JSON.stringify({}, null, 2)}\n`, "utf8", ); - await writeFile(path.join(appPath, "server.ts"), "export default { fetch: () => new Response('ok') };\n", "utf8"); - await writeFile(path.join(appPath, "next.config.cjs"), "module.exports = { output: 'standalone' };\n", "utf8"); + await writeFile( + path.join(appPath, "server.ts"), + "export default { fetch: () => new Response('ok') };\n", + "utf8", + ); + await writeFile( + path.join(appPath, "next.config.cjs"), + "module.exports = { output: 'standalone' };\n", + "utf8", + ); - await expect(resolvePreviewBuildStrategy({ - appPath, - entrypoint: "server.ts", - buildType: "auto", - })).resolves.toMatchObject({ + await expect( + resolvePreviewBuildStrategy({ + appPath, + entrypoint: "server.ts", + buildType: "auto", + }), + ).resolves.toMatchObject({ buildType: "bun", }); }); @@ -444,15 +591,19 @@ describe("preview build strategy", () => { await mkdir(appPath, { recursive: true }); await writeFile( path.join(appPath, "package.json"), - JSON.stringify({ - packageManager: "npm@10.0.0", - scripts: { - build: "node build.mjs", - }, - dependencies: { - next: "15.0.0", + JSON.stringify( + { + packageManager: "npm@10.0.0", + scripts: { + build: "node build.mjs", + }, + dependencies: { + next: "15.0.0", + }, }, - }, null, 2), + null, + 2, + ), "utf8", ); await writeFile( @@ -469,7 +620,9 @@ describe("preview build strategy", () => { "utf8", ); - const { executePreviewBuild } = await import("../src/lib/app/preview-build"); + const { executePreviewBuild } = await import( + "../src/lib/app/preview-build" + ); const result = await executePreviewBuild({ appPath, buildType: "nextjs", @@ -477,8 +630,18 @@ describe("preview build strategy", () => { expect(result.buildType).toBe("nextjs"); expect(result.artifact.entrypoint).toBe("server.js"); - await expect(readFile(path.join(result.artifact.directory, ".next", "static", "client.js"), "utf8")).resolves.toContain("static"); - await expect(readFile(path.join(result.artifact.directory, "public", "hello.txt"), "utf8")).resolves.toContain("hello"); + await expect( + readFile( + path.join(result.artifact.directory, ".next", "static", "client.js"), + "utf8", + ), + ).resolves.toContain("static"); + await expect( + readFile( + path.join(result.artifact.directory, "public", "hello.txt"), + "utf8", + ), + ).resolves.toContain("hello"); await result.artifact.cleanup?.(); }); @@ -488,9 +651,15 @@ describe("preview build strategy", () => { const outputDir = path.join(appPath, ".next", "standalone"); await mkdir(outputDir, { recursive: true }); - await writeFile(path.join(outputDir, "server.js"), "console.log('prebuilt');\n", "utf8"); + await writeFile( + path.join(outputDir, "server.js"), + "console.log('prebuilt');\n", + "utf8", + ); - const { executePreviewBuild } = await import("../src/lib/app/preview-build"); + const { executePreviewBuild } = await import( + "../src/lib/app/preview-build" + ); const result = await executePreviewBuild({ appPath, buildType: "nextjs", @@ -504,7 +673,9 @@ describe("preview build strategy", () => { expect(result.buildType).toBe("nextjs"); expect(result.artifact.entrypoint).toBe("server.js"); - await expect(readFile(path.join(result.artifact.directory, "server.js"), "utf8")).resolves.toContain("prebuilt"); + await expect( + readFile(path.join(result.artifact.directory, "server.js"), "utf8"), + ).resolves.toContain("prebuilt"); await result.artifact.cleanup?.(); }); @@ -514,17 +685,37 @@ describe("preview build strategy", () => { const standaloneDir = path.join(appPath, ".next", "standalone"); const nextBin = path.join(appPath, "node_modules", ".bin", "next"); - await mkdir(path.join(standaloneDir, ".next", "static"), { recursive: true }); + await mkdir(path.join(standaloneDir, ".next", "static"), { + recursive: true, + }); await mkdir(path.join(appPath, ".next", "static"), { recursive: true }); - await writeFile(path.join(appPath, ".next", "static", "client.js"), "console.log('static');\n", "utf8"); + await writeFile( + path.join(appPath, ".next", "static", "client.js"), + "console.log('static');\n", + "utf8", + ); await mkdir(path.join(appPath, "public"), { recursive: true }); - await writeFile(path.join(appPath, "public", "hello.txt"), "hello\n", "utf8"); + await writeFile( + path.join(appPath, "public", "hello.txt"), + "hello\n", + "utf8", + ); await mkdir(path.dirname(nextBin), { recursive: true }); - await writeFile(path.join(appPath, "next.config.ts"), "export default { output: 'standalone' };\n", "utf8"); - await writeFile(path.join(standaloneDir, "server.js"), "console.log('next');\n", "utf8"); + await writeFile( + path.join(appPath, "next.config.ts"), + "export default { output: 'standalone' };\n", + "utf8", + ); + await writeFile( + path.join(standaloneDir, "server.js"), + "console.log('next');\n", + "utf8", + ); await symlink("/usr/bin/true", nextBin); - const { executePreviewBuild } = await import("../src/lib/app/preview-build"); + const { executePreviewBuild } = await import( + "../src/lib/app/preview-build" + ); const result = await executePreviewBuild({ appPath, buildType: "nextjs", @@ -533,13 +724,25 @@ describe("preview build strategy", () => { expect(result.buildType).toBe("nextjs"); expect(result.artifact.entrypoint).toBe("server.js"); expect(result.artifact.defaultPortMapping).toEqual({ http: 3000 }); - await expect(readFile(path.join(result.artifact.directory, ".next", "static", "client.js"), "utf8")).resolves.toContain("static"); - await expect(readFile(path.join(result.artifact.directory, "public", "hello.txt"), "utf8")).resolves.toContain("hello"); + await expect( + readFile( + path.join(result.artifact.directory, ".next", "static", "client.js"), + "utf8", + ), + ).resolves.toContain("static"); + await expect( + readFile( + path.join(result.artifact.directory, "public", "hello.txt"), + "utf8", + ), + ).resolves.toContain("hello"); await result.artifact.cleanup?.(); }); it("materializes symlinks that point back to the source app directory", async () => { - const { normalizeArtifactSymlinks } = await import("../src/lib/app/preview-build"); + const { normalizeArtifactSymlinks } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); const artifactDir = path.join(cwd, "artifact"); @@ -553,7 +756,11 @@ describe("preview build strategy", () => { ); await mkdir(sourceTarget, { recursive: true }); - await writeFile(path.join(sourceTarget, "index.js"), "export const value = 1;\n", "utf8"); + await writeFile( + path.join(sourceTarget, "index.js"), + "export const value = 1;\n", + "utf8", + ); await mkdir(path.dirname(copiedLink), { recursive: true }); await symlink(sourceTarget, copiedLink, "dir"); @@ -561,13 +768,15 @@ describe("preview build strategy", () => { await normalizeArtifactSymlinks(artifactDir, appPath); expect((await lstat(copiedLink)).isSymbolicLink()).toBe(false); - await expect(readFile(path.join(copiedLink, "index.js"), "utf8")).resolves.toContain( - "value = 1", - ); + await expect( + readFile(path.join(copiedLink, "index.js"), "utf8"), + ).resolves.toContain("value = 1"); }); it("stages Next.js standalone artifacts by materializing symlinks and falling back to app node_modules", async () => { - const { stageNextjsStandaloneArtifact } = await import("../src/lib/app/preview-build"); + const { stageNextjsStandaloneArtifact } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); const standaloneDir = path.join(appPath, ".next", "standalone"); @@ -591,14 +800,26 @@ describe("preview build strategy", () => { ); await mkdir(standaloneTarget, { recursive: true }); - await writeFile(path.join(standaloneTarget, "index.js"), "export const sharp = true;\n", "utf8"); + await writeFile( + path.join(standaloneTarget, "index.js"), + "export const sharp = true;\n", + "utf8", + ); await mkdir(path.dirname(standaloneLink), { recursive: true }); await symlink("../sharp@0.34.5/node_modules/sharp", standaloneLink, "dir"); await mkdir(appFallbackTarget, { recursive: true }); - await writeFile(path.join(appFallbackTarget, "index.js"), "export const semver = true;\n", "utf8"); + await writeFile( + path.join(appFallbackTarget, "index.js"), + "export const semver = true;\n", + "utf8", + ); await mkdir(path.dirname(standaloneMissingLink), { recursive: true }); - await symlink("../semver@6.3.1/node_modules/semver", standaloneMissingLink, "dir"); + await symlink( + "../semver@6.3.1/node_modules/semver", + standaloneMissingLink, + "dir", + ); await stageNextjsStandaloneArtifact({ standaloneDir, @@ -617,12 +838,18 @@ describe("preview build strategy", () => { expect((await lstat(copiedStandaloneTarget)).isSymbolicLink()).toBe(false); expect((await lstat(copiedFallbackTarget)).isSymbolicLink()).toBe(false); - await expect(readFile(path.join(copiedStandaloneTarget, "index.js"), "utf8")).resolves.toContain("sharp = true"); - await expect(readFile(path.join(copiedFallbackTarget, "index.js"), "utf8")).resolves.toContain("semver = true"); + await expect( + readFile(path.join(copiedStandaloneTarget, "index.js"), "utf8"), + ).resolves.toContain("sharp = true"); + await expect( + readFile(path.join(copiedFallbackTarget, "index.js"), "utf8"), + ).resolves.toContain("semver = true"); }); it("stages Next.js standalone symlinks that resolve through the monorepo root", async () => { - const { stageNextjsStandaloneArtifact } = await import("../src/lib/app/preview-build"); + const { stageNextjsStandaloneArtifact } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const repoRoot = path.join(cwd, "repo"); const appPath = path.join(repoRoot, "apps", "web"); @@ -633,9 +860,17 @@ describe("preview build strategy", () => { await mkdir(path.join(repoRoot, ".git"), { recursive: true }); await mkdir(rootDependency, { recursive: true }); - await writeFile(path.join(rootDependency, "index.js"), "export const pg = true;\n", "utf8"); + await writeFile( + path.join(rootDependency, "index.js"), + "export const pg = true;\n", + "utf8", + ); await mkdir(path.dirname(standaloneLink), { recursive: true }); - await symlink(path.relative(path.dirname(standaloneLink), rootDependency), standaloneLink, "dir"); + await symlink( + path.relative(path.dirname(standaloneLink), rootDependency), + standaloneLink, + "dir", + ); await stageNextjsStandaloneArtifact({ standaloneDir, @@ -646,11 +881,15 @@ describe("preview build strategy", () => { const copiedDependency = path.join(artifactDir, "node_modules", "pg"); expect((await lstat(copiedDependency)).isSymbolicLink()).toBe(false); - await expect(readFile(path.join(copiedDependency, "index.js"), "utf8")).resolves.toContain("pg = true"); + await expect( + readFile(path.join(copiedDependency, "index.js"), "utf8"), + ).resolves.toContain("pg = true"); }); it("keeps pnpm transitive dependencies resolvable after flattening Next.js standalone packages", async () => { - const { stageNextjsStandaloneArtifact } = await import("../src/lib/app/preview-build"); + const { stageNextjsStandaloneArtifact } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); const standaloneDir = path.join(appPath, ".next", "standalone"); @@ -669,7 +908,9 @@ describe("preview build strategy", () => { "node_modules/.pnpm/node_modules/@swc/helpers", ); - await mkdir(path.join(nextStorePackage, "dist/shared/lib"), { recursive: true }); + await mkdir(path.join(nextStorePackage, "dist/shared/lib"), { + recursive: true, + }); await writeFile( path.join(nextStorePackage, "dist/shared/lib/constants.js"), "module.exports = require('@swc/helpers/_/_interop_require_default');\n", @@ -685,7 +926,11 @@ describe("preview build strategy", () => { "utf8", ); await mkdir(path.dirname(swcHoistedLink), { recursive: true }); - await symlink("../../@swc+helpers@0.5.15/node_modules/@swc/helpers", swcHoistedLink, "dir"); + await symlink( + "../../@swc+helpers@0.5.15/node_modules/@swc/helpers", + swcHoistedLink, + "dir", + ); await stageNextjsStandaloneArtifact({ standaloneDir, @@ -693,14 +938,21 @@ describe("preview build strategy", () => { appPath, }); - const constants = path.join(artifactDir, "node_modules/next/dist/shared/lib/constants.js"); + const constants = path.join( + artifactDir, + "node_modules/next/dist/shared/lib/constants.js", + ); const requireFromNext = createRequire(constants); - expect(() => requireFromNext.resolve("@swc/helpers/_/_interop_require_default")).not.toThrow(); + expect(() => + requireFromNext.resolve("@swc/helpers/_/_interop_require_default"), + ).not.toThrow(); }); it("places public and .next/static next to server.js when the entrypoint is nested (monorepo)", async () => { - const { restageNextjsArtifact } = await import("../src/lib/app/preview-build"); + const { restageNextjsArtifact } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "repo", "apps", "web"); const standaloneDir = path.join(appPath, ".next", "standalone"); @@ -709,13 +961,25 @@ describe("preview build strategy", () => { await mkdir(path.join(cwd, "repo", ".git"), { recursive: true }); await mkdir(nestedServerDir, { recursive: true }); - await writeFile(path.join(nestedServerDir, "server.js"), "// nested server\n", "utf8"); + await writeFile( + path.join(nestedServerDir, "server.js"), + "// nested server\n", + "utf8", + ); await mkdir(path.join(standaloneDir, "node_modules"), { recursive: true }); await mkdir(path.join(appPath, "public"), { recursive: true }); - await writeFile(path.join(appPath, "public", "hello.txt"), "hello\n", "utf8"); + await writeFile( + path.join(appPath, "public", "hello.txt"), + "hello\n", + "utf8", + ); await mkdir(path.join(appPath, ".next", "static"), { recursive: true }); - await writeFile(path.join(appPath, ".next", "static", "client.js"), "// static\n", "utf8"); + await writeFile( + path.join(appPath, ".next", "static", "client.js"), + "// static\n", + "utf8", + ); // Seed an existing (incorrect) artifact directory to mirror what the SDK // produces before the CLI re-stages it. @@ -727,15 +991,23 @@ describe("preview build strategy", () => { ); await expect( - readFile(path.join(artifactDir, "apps", "web", "public", "hello.txt"), "utf8"), + readFile( + path.join(artifactDir, "apps", "web", "public", "hello.txt"), + "utf8", + ), ).resolves.toContain("hello"); await expect( - readFile(path.join(artifactDir, "apps", "web", ".next", "static", "client.js"), "utf8"), + readFile( + path.join(artifactDir, "apps", "web", ".next", "static", "client.js"), + "utf8", + ), ).resolves.toContain("static"); }); it("drops dangling pnpm hoist symlinks when staging Next.js standalone artifacts", async () => { - const { stageNextjsStandaloneArtifact } = await import("../src/lib/app/preview-build"); + const { stageNextjsStandaloneArtifact } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); const standaloneDir = path.join(appPath, ".next", "standalone"); @@ -750,7 +1022,11 @@ describe("preview build strategy", () => { "node_modules/.pnpm/node_modules/real", ); await mkdir(realTarget, { recursive: true }); - await writeFile(path.join(realTarget, "index.js"), "export const real = true;\n", "utf8"); + await writeFile( + path.join(realTarget, "index.js"), + "export const real = true;\n", + "utf8", + ); await mkdir(path.dirname(realLink), { recursive: true }); await symlink("../real@1.0.0/node_modules/real", realLink, "dir"); @@ -758,14 +1034,22 @@ describe("preview build strategy", () => { standaloneDir, "node_modules/.pnpm/node_modules/missing-pkg", ); - await symlink("../missing-pkg@1.0.0/node_modules/missing-pkg", danglingLink, "dir"); + await symlink( + "../missing-pkg@1.0.0/node_modules/missing-pkg", + danglingLink, + "dir", + ); const danglingScopedLink = path.join( standaloneDir, "node_modules/.pnpm/node_modules/@scope/missing-pkg", ); await mkdir(path.dirname(danglingScopedLink), { recursive: true }); - await symlink("../../@scope+missing-pkg@1.0.0/node_modules/@scope/missing-pkg", danglingScopedLink, "dir"); + await symlink( + "../../@scope+missing-pkg@1.0.0/node_modules/@scope/missing-pkg", + danglingScopedLink, + "dir", + ); await stageNextjsStandaloneArtifact({ standaloneDir, @@ -777,7 +1061,9 @@ describe("preview build strategy", () => { readFile(path.join(artifactDir, "node_modules/real/index.js"), "utf8"), ).resolves.toContain("real = true"); await expect( - lstat(path.join(artifactDir, "node_modules/.pnpm/node_modules/missing-pkg")), + lstat( + path.join(artifactDir, "node_modules/.pnpm/node_modules/missing-pkg"), + ), ).rejects.toThrow(); await expect( lstat(path.join(artifactDir, "node_modules/missing-pkg")), @@ -788,25 +1074,39 @@ describe("preview build strategy", () => { }); it("still rejects dangling Next.js standalone symlinks outside the pnpm hoist layer", async () => { - const { stageNextjsStandaloneArtifact } = await import("../src/lib/app/preview-build"); + const { stageNextjsStandaloneArtifact } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); const standaloneDir = path.join(appPath, ".next", "standalone"); const artifactDir = path.join(cwd, "artifact"); - const brokenTopLevelLink = path.join(standaloneDir, "node_modules", "missing-direct"); + const brokenTopLevelLink = path.join( + standaloneDir, + "node_modules", + "missing-direct", + ); await mkdir(path.dirname(brokenTopLevelLink), { recursive: true }); - await symlink(".pnpm/missing-direct@1.0.0/node_modules/missing-direct", brokenTopLevelLink, "dir"); + await symlink( + ".pnpm/missing-direct@1.0.0/node_modules/missing-direct", + brokenTopLevelLink, + "dir", + ); - await expect(stageNextjsStandaloneArtifact({ - standaloneDir, - artifactDir, - appPath, - })).rejects.toThrow("symlink target is missing"); + await expect( + stageNextjsStandaloneArtifact({ + standaloneDir, + artifactDir, + appPath, + }), + ).rejects.toThrow("symlink target is missing"); }); it("rejects Next.js standalone symlinks that escape the app directory", async () => { - const { stageNextjsStandaloneArtifact } = await import("../src/lib/app/preview-build"); + const { stageNextjsStandaloneArtifact } = await import( + "../src/lib/app/preview-build" + ); const cwd = await createTempCwd(); const appPath = path.join(cwd, "app"); const standaloneDir = path.join(appPath, ".next", "standalone"); @@ -816,14 +1116,20 @@ describe("preview build strategy", () => { await mkdir(path.join(appPath, ".git"), { recursive: true }); await mkdir(escapeTarget, { recursive: true }); - await writeFile(path.join(escapeTarget, "index.js"), "export const escaped = true;\n", "utf8"); + await writeFile( + path.join(escapeTarget, "index.js"), + "export const escaped = true;\n", + "utf8", + ); await mkdir(path.dirname(escapeLink), { recursive: true }); await symlink(escapeTarget, escapeLink, "dir"); - await expect(stageNextjsStandaloneArtifact({ - standaloneDir, - artifactDir, - appPath, - })).rejects.toThrow("escapes the app directory"); + await expect( + stageNextjsStandaloneArtifact({ + standaloneDir, + artifactDir, + appPath, + }), + ).rejects.toThrow("escapes the app directory"); }); }); diff --git a/packages/cli/tests/app-bun-compat.test.ts b/packages/cli/tests/app-bun-compat.test.ts index 47a36a6..7b7df08 100644 --- a/packages/cli/tests/app-bun-compat.test.ts +++ b/packages/cli/tests/app-bun-compat.test.ts @@ -11,10 +11,12 @@ afterEach(() => { vi.restoreAllMocks(); }); -function mockBuildStrategy(createInstance: (options: object) => object = () => ({ - canBuild: vi.fn(), - execute: vi.fn(), -})) { +function mockBuildStrategy( + createInstance: (options: object) => object = () => ({ + canBuild: vi.fn(), + execute: vi.fn(), + }), +) { return vi.fn().mockImplementation(function (options: object) { return createInstance(options); }); @@ -26,19 +28,29 @@ describe("bun compatibility", () => { await writeFile( path.join(cwd, "package.json"), - JSON.stringify({ - module: "index.ts", - devDependencies: { - "@types/bun": "latest", + JSON.stringify( + { + module: "index.ts", + devDependencies: { + "@types/bun": "latest", + }, }, - }, null, 2), + null, + 2, + ), + "utf8", + ); + await writeFile( + path.join(cwd, "index.ts"), + "console.log('hello');\n", "utf8", ); - await writeFile(path.join(cwd, "index.ts"), "console.log('hello');\n", "utf8"); const { resolveBunEntrypoint } = await import("../src/lib/app/bun-project"); - await expect(resolveBunEntrypoint(cwd, undefined)).resolves.toBe("index.ts"); + await expect(resolveBunEntrypoint(cwd, undefined)).resolves.toBe( + "index.ts", + ); }); it("rejects Bun package reads when the command signal is already aborted", async () => { @@ -49,7 +61,9 @@ describe("bun compatibility", () => { const { readBunPackageJson } = await import("../src/lib/app/bun-project"); - await expect(readBunPackageJson(cwd, controller.signal)).rejects.toBe(reason); + await expect(readBunPackageJson(cwd, controller.signal)).rejects.toBe( + reason, + ); }); it("detects a Bun project when package.json uses module instead of main", async () => { @@ -57,18 +71,26 @@ describe("bun compatibility", () => { await writeFile( path.join(cwd, "package.json"), - JSON.stringify({ - module: "index.ts", - devDependencies: { - "@types/bun": "latest", - }, - scripts: { - dev: "bun --watch index.ts", + JSON.stringify( + { + module: "index.ts", + devDependencies: { + "@types/bun": "latest", + }, + scripts: { + dev: "bun --watch index.ts", + }, }, - }, null, 2), + null, + 2, + ), + "utf8", + ); + await writeFile( + path.join(cwd, "index.ts"), + "console.log('hello');\n", "utf8", ); - await writeFile(path.join(cwd, "index.ts"), "console.log('hello');\n", "utf8"); const { detectLocalBuildType } = await import("../src/lib/app/local-dev"); @@ -80,15 +102,23 @@ describe("bun compatibility", () => { await writeFile( path.join(cwd, "package.json"), - JSON.stringify({ - module: "index.ts", - devDependencies: { - "@types/bun": "latest", + JSON.stringify( + { + module: "index.ts", + devDependencies: { + "@types/bun": "latest", + }, }, - }, null, 2), + null, + 2, + ), + "utf8", + ); + await writeFile( + path.join(cwd, "index.ts"), + "console.log('hello');\n", "utf8", ); - await writeFile(path.join(cwd, "index.ts"), "console.log('hello');\n", "utf8"); const bunBuild = mockBuildStrategy((options: object) => ({ options, @@ -112,7 +142,9 @@ describe("bun compatibility", () => { TanstackStartBuild: otherFrameworkBuild, })); - const { resolvePreviewBuildStrategy } = await import("../src/lib/app/preview-build"); + const { resolvePreviewBuildStrategy } = await import( + "../src/lib/app/preview-build" + ); const result = await resolvePreviewBuildStrategy({ appPath: cwd, @@ -159,7 +191,9 @@ describe("bun compatibility", () => { TanstackStartBuild: tanstackStartBuild, })); - const { resolvePreviewBuildStrategy } = await import("../src/lib/app/preview-build"); + const { resolvePreviewBuildStrategy } = await import( + "../src/lib/app/preview-build" + ); const result = await resolvePreviewBuildStrategy({ appPath: cwd, @@ -190,19 +224,25 @@ describe("bun compatibility", () => { TanstackStartBuild: buildStrategy, })); - const { resolvePreviewBuildStrategy } = await import("../src/lib/app/preview-build"); - - await expect(resolvePreviewBuildStrategy({ - appPath: cwd, - buildType: "astro", - entrypoint: undefined, - })).resolves.toMatchObject({ buildType: "astro" }); + const { resolvePreviewBuildStrategy } = await import( + "../src/lib/app/preview-build" + ); - await expect(resolvePreviewBuildStrategy({ - appPath: cwd, - buildType: "tanstack-start", - entrypoint: undefined, - })).resolves.toMatchObject({ buildType: "tanstack-start" }); + await expect( + resolvePreviewBuildStrategy({ + appPath: cwd, + buildType: "astro", + entrypoint: undefined, + }), + ).resolves.toMatchObject({ buildType: "astro" }); + + await expect( + resolvePreviewBuildStrategy({ + appPath: cwd, + buildType: "tanstack-start", + entrypoint: undefined, + }), + ).resolves.toMatchObject({ buildType: "tanstack-start" }); }); it("still lets an explicit Bun entrypoint override package.json module", async () => { @@ -210,19 +250,33 @@ describe("bun compatibility", () => { await writeFile( path.join(cwd, "package.json"), - JSON.stringify({ - module: "index.ts", - devDependencies: { - "@types/bun": "latest", + JSON.stringify( + { + module: "index.ts", + devDependencies: { + "@types/bun": "latest", + }, }, - }, null, 2), + null, + 2, + ), + "utf8", + ); + await writeFile( + path.join(cwd, "index.ts"), + "console.log('hello');\n", + "utf8", + ); + await writeFile( + path.join(cwd, "server.ts"), + "console.log('server');\n", "utf8", ); - await writeFile(path.join(cwd, "index.ts"), "console.log('hello');\n", "utf8"); - await writeFile(path.join(cwd, "server.ts"), "console.log('server');\n", "utf8"); const { resolveBunEntrypoint } = await import("../src/lib/app/bun-project"); - await expect(resolveBunEntrypoint(cwd, "server.ts")).resolves.toBe("server.ts"); + await expect(resolveBunEntrypoint(cwd, "server.ts")).resolves.toBe( + "server.ts", + ); }); }); diff --git a/packages/cli/tests/app-controller.test.ts b/packages/cli/tests/app-controller.test.ts index ea8eb6b..cc1bd4c 100644 --- a/packages/cli/tests/app-controller.test.ts +++ b/packages/cli/tests/app-controller.test.ts @@ -3,7 +3,10 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createProjectClient, createResolveBranch } from "./helpers/mock-factories"; +import { + createProjectClient, + createResolveBranch, +} from "./helpers/mock-factories"; beforeEach(() => { process.env.PRISMA_CLI_TEST_REMEMBER_PROJECT_ID = "proj_123"; @@ -42,7 +45,9 @@ afterEach(() => { vi.restoreAllMocks(); }); -function withBranchDatabaseProviderDefaults>(provider: T) { +function withBranchDatabaseProviderDefaults>( + provider: T, +) { return { createBranchDatabase: vi.fn(), deleteBranchDatabase: vi.fn(), @@ -77,15 +82,29 @@ function expectedAppVerboseContext() { }; } -function createDomain(overrides: Partial<{ - id: string; - hostname: string; - computeServiceId: string; - status: "pending_dns" | "verifying" | "verified_routing_blocked" | "provisioning_tls" | "active" | "failed" | "removing"; - failureReason: string | null; - failureCategory: "dns" | "acme" | "storage" | "unknown" | null; - dnsRecords: Array<{ type: string; name: string; value: string; ttl: number | null }>; -}> = {}) { +function createDomain( + overrides: Partial<{ + id: string; + hostname: string; + computeServiceId: string; + status: + | "pending_dns" + | "verifying" + | "verified_routing_blocked" + | "provisioning_tls" + | "active" + | "failed" + | "removing"; + failureReason: string | null; + failureCategory: "dns" | "acme" | "storage" | "unknown" | null; + dnsRecords: Array<{ + type: string; + name: string; + value: string; + ttl: number | null; + }>; + }> = {}, +) { const hostname = overrides.hostname ?? "shop.acme.com"; return { id: overrides.id ?? "dom_123", @@ -120,16 +139,24 @@ async function writePackageJson( devDependencies?: Record; }, ): Promise { - await writeFile(path.join(cwd, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`); + await writeFile( + path.join(cwd, "package.json"), + `${JSON.stringify(packageJson, null, 2)}\n`, + ); } async function writeGitBranch(cwd: string, branchName: string): Promise { await mkdir(path.join(cwd, ".git"), { recursive: true }); - await writeFile(path.join(cwd, ".git", "HEAD"), `ref: refs/heads/${branchName}\n`); + await writeFile( + path.join(cwd, ".git", "HEAD"), + `ref: refs/heads/${branchName}\n`, + ); } async function readLocalPin(cwd: string): Promise { - return JSON.parse(await readFile(path.join(cwd, ".prisma/local.json"), "utf8")); + return JSON.parse( + await readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ); } async function readPrismaAppConfig(cwd: string): Promise { @@ -151,8 +178,18 @@ describe("app controller", () => { it("deploy selects the correct existing app when --app is provided", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_2", name: "billing", region: "eu-west-3", liveDeploymentId: null }, - { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: "dep_live" }, + { + id: "app_2", + name: "billing", + region: "eu-west-3", + liveDeploymentId: null, + }, + { + id: "app_1", + name: "hello-world", + region: "eu-west-3", + liveDeploymentId: "dep_live", + }, ]); const deployApp = vi.fn().mockResolvedValue({ projectId: "proj_123", @@ -173,16 +210,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -232,7 +273,9 @@ describe("app controller", () => { url: "https://hello-world.prisma.app", }, }); - await expect(context.stateStore.readSelectedApp("proj_123")).resolves.toEqual({ + await expect( + context.stateStore.readSelectedApp("proj_123"), + ).resolves.toEqual({ id: "app_1", name: "hello-world", }); @@ -240,7 +283,13 @@ describe("app controller", () => { it("does not treat branch name as production authority", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); - const app = { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: "dep_live", liveUrl: null }; + const app = { + id: "app_1", + name: "hello-world", + region: "eu-west-3", + liveDeploymentId: "dep_live", + liveUrl: null, + }; const listApps = vi.fn().mockResolvedValue([app]); const listDeployments = vi.fn(); const deployApp = vi.fn().mockResolvedValue({ @@ -262,19 +311,26 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments, - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments, + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); - const { context } = await createTestCommandContext({ cwd, stateDir: path.join(cwd, ".state") }); + const { context } = await createTestCommandContext({ + cwd, + stateDir: path.join(cwd, ".state"), + }); const result = await runAppDeploy(context, "hello-world", { projectRef: "proj_123", @@ -294,7 +350,13 @@ describe("app controller", () => { it("forwards deploy build options and HTTP port overrides to the provider", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const deployApp = vi.fn().mockResolvedValue({ projectId: "proj_123", @@ -316,21 +378,25 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - removeApp: vi.fn(), - promoteDeployment: vi.fn(), - deployApp, - updateAppEnv: vi.fn(), - listAppEnvNames: vi.fn(), - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + removeApp: vi.fn(), + promoteDeployment: vi.fn(), + deployApp, + updateAppEnv: vi.fn(), + listAppEnvNames: vi.fn(), + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -368,7 +434,13 @@ describe("app controller", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const activeDomain = createDomain({ status: "active" }); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "shop", region: "eu-central-1", liveDeploymentId: "dep_live", liveUrl: "https://shop.prisma.app" }, + { + id: "app_1", + name: "shop", + region: "eu-central-1", + liveDeploymentId: "dep_live", + liveUrl: "https://shop.prisma.app", + }, ]); const addDomain = vi.fn().mockResolvedValue({ domain: activeDomain, @@ -380,20 +452,27 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal< + typeof import("../src/lib/app/preview-provider") + >(); return { ...actual, - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - listDomains: vi.fn(), - addDomain, - retryDomain, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + listDomains: vi.fn(), + addDomain, + retryDomain, + }), + ), }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainAdd } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -437,12 +516,14 @@ describe("app controller", () => { domain: { hostname: "shop.acme.com", status: "active", - dnsRecords: [{ - type: "CNAME", - name: "shop.acme.com", - value: "switchboard.fra.prisma.build", - ttl: 300, - }], + dnsRecords: [ + { + type: "CNAME", + name: "shop.acme.com", + value: "switchboard.fra.prisma.build", + ttl: 300, + }, + ], }, existing: true, }); @@ -452,7 +533,13 @@ describe("app controller", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const domain = createDomain({ status: "active" }); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "shop", region: "eu-central-1", liveDeploymentId: "dep_live", liveUrl: "https://shop.prisma.app" }, + { + id: "app_1", + name: "shop", + region: "eu-central-1", + liveDeploymentId: "dep_live", + liveUrl: "https://shop.prisma.app", + }, ]); const addDomain = vi.fn().mockResolvedValue({ domain, @@ -463,18 +550,25 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal< + typeof import("../src/lib/app/preview-provider") + >(); return { ...actual, - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - addDomain, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + addDomain, + }), + ), }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainAdd } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { @@ -515,19 +609,26 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal< + typeof import("../src/lib/app/preview-provider") + >(); return { ...actual, - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject, - listApps, - addDomain, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject, + listApps, + addDomain, + }), + ), }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainAdd } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writePackageJson(cwd, { name: "acme-dashboard" }); @@ -542,9 +643,11 @@ describe("app controller", () => { }, }); - await expect(runAppDomainAdd(context, "shop.acme.com", { - appName: "shop", - })).rejects.toMatchObject({ + await expect( + runAppDomainAdd(context, "shop.acme.com", { + appName: "shop", + }), + ).rejects.toMatchObject({ code: "PROJECT_SETUP_REQUIRED", domain: "project", meta: { @@ -587,18 +690,25 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal< + typeof import("../src/lib/app/preview-provider") + >(); return { ...actual, - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - addDomain, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + addDomain, + }), + ), }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainAdd } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -635,24 +745,33 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", async (importOriginal) => { - const actual = await importOriginal(); - const addDomain = vi.fn().mockRejectedValue(new actual.PreviewDomainApiError({ - summary: "Failed to add custom domain", - status: 409, - message: "Domain quota exceeded.", - hint: "This compute service has reached the maximum of 3 custom domains.", - })); + const actual = + await importOriginal< + typeof import("../src/lib/app/preview-provider") + >(); + const addDomain = vi.fn().mockRejectedValue( + new actual.PreviewDomainApiError({ + summary: "Failed to add custom domain", + status: 409, + message: "Domain quota exceeded.", + hint: "This compute service has reached the maximum of 3 custom domains.", + }), + ); return { ...actual, - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - addDomain, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + addDomain, + }), + ), }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainAdd } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -665,10 +784,12 @@ describe("app controller", () => { }, }); - await expect(runAppDomainAdd(context, "shop.acme.com", { - projectRef: "proj_123", - appName: "shop", - })).rejects.toMatchObject({ + await expect( + runAppDomainAdd(context, "shop.acme.com", { + projectRef: "proj_123", + appName: "shop", + }), + ).rejects.toMatchObject({ code: "DOMAIN_QUOTA_EXCEEDED", domain: "app", }); @@ -690,24 +811,33 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", async (importOriginal) => { - const actual = await importOriginal(); - const addDomain = vi.fn().mockRejectedValue(new actual.PreviewDomainApiError({ - summary: "Failed to add custom domain", - status: 409, - message: "Hostname already registered.", - hint: "This hostname is already registered to another compute service.", - })); + const actual = + await importOriginal< + typeof import("../src/lib/app/preview-provider") + >(); + const addDomain = vi.fn().mockRejectedValue( + new actual.PreviewDomainApiError({ + summary: "Failed to add custom domain", + status: 409, + message: "Hostname already registered.", + hint: "This hostname is already registered to another compute service.", + }), + ); return { ...actual, - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - addDomain, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + addDomain, + }), + ), }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainAdd } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -720,13 +850,15 @@ describe("app controller", () => { }, }); - await expect(runAppDomainAdd(context, "shop.acme.com", { - projectRef: "proj_123", - appName: "shop", - })).rejects.toMatchObject({ + await expect( + runAppDomainAdd(context, "shop.acme.com", { + projectRef: "proj_123", + appName: "shop", + }), + ).rejects.toMatchObject({ code: "DOMAIN_ALREADY_REGISTERED", domain: "app", - summary: "Custom domain \"shop.acme.com\" is already registered", + summary: 'Custom domain "shop.acme.com" is already registered', fix: "Select the app that owns this hostname and remove it there, or contact support if you cannot access it.", nextSteps: [ "Select the owning app and remove shop.acme.com there.", @@ -751,24 +883,33 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", async (importOriginal) => { - const actual = await importOriginal(); - const addDomain = vi.fn().mockRejectedValue(new actual.PreviewDomainApiError({ - summary: "Failed to add custom domain", - status: 400, - message: "No CNAME or A/AAAA records found for hostname.", - hint: "DNS verification failed: ensure the hostname CNAMEs to switchboard.fra.prisma.build.", - })); + const actual = + await importOriginal< + typeof import("../src/lib/app/preview-provider") + >(); + const addDomain = vi.fn().mockRejectedValue( + new actual.PreviewDomainApiError({ + summary: "Failed to add custom domain", + status: 400, + message: "No CNAME or A/AAAA records found for hostname.", + hint: "DNS verification failed: ensure the hostname CNAMEs to switchboard.fra.prisma.build.", + }), + ); return { ...actual, - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - addDomain, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + addDomain, + }), + ), }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainAdd } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -781,10 +922,12 @@ describe("app controller", () => { }, }); - await expect(runAppDomainAdd(context, "compute-test.amanv.dev", { - projectRef: "proj_123", - appName: "shop", - })).rejects.toMatchObject({ + await expect( + runAppDomainAdd(context, "compute-test.amanv.dev", { + projectRef: "proj_123", + appName: "shop", + }), + ).rejects.toMatchObject({ code: "DOMAIN_DNS_NOT_CONFIGURED", domain: "app", fix: "Add CNAME compute-test.amanv.dev -> switchboard.fra.prisma.build at your DNS provider, then rerun the domain command.", @@ -811,24 +954,33 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", async (importOriginal) => { - const actual = await importOriginal(); - const addDomain = vi.fn().mockRejectedValue(new actual.PreviewDomainApiError({ - summary: "Failed to add custom domain", - status: 400, - message: "DNS is not configured for hostname compute-test.amanv.dev.", - hint: "DNS verification failed.", - })); + const actual = + await importOriginal< + typeof import("../src/lib/app/preview-provider") + >(); + const addDomain = vi.fn().mockRejectedValue( + new actual.PreviewDomainApiError({ + summary: "Failed to add custom domain", + status: 400, + message: "DNS is not configured for hostname compute-test.amanv.dev.", + hint: "DNS verification failed.", + }), + ); return { ...actual, - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - addDomain, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + addDomain, + }), + ), }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainAdd } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -841,10 +993,12 @@ describe("app controller", () => { }, }); - await expect(runAppDomainAdd(context, "compute-test.amanv.dev", { - projectRef: "proj_123", - appName: "shop", - })).rejects.toMatchObject({ + await expect( + runAppDomainAdd(context, "compute-test.amanv.dev", { + projectRef: "proj_123", + appName: "shop", + }), + ).rejects.toMatchObject({ code: "DOMAIN_DNS_NOT_CONFIGURED", domain: "app", fix: "The platform did not return the required DNS target. Re-run with --trace for the underlying API response details.", @@ -869,18 +1023,25 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal< + typeof import("../src/lib/app/preview-provider") + >(); return { ...actual, - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - listDomains, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + listDomains, + }), + ), }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainRemove } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -894,17 +1055,21 @@ describe("app controller", () => { }, }); - await expect(runAppDomainRemove(context, "shop.acme.com", { - projectRef: "proj_123", - appName: "shop", - })).rejects.toMatchObject({ + await expect( + runAppDomainRemove(context, "shop.acme.com", { + projectRef: "proj_123", + appName: "shop", + }), + ).rejects.toMatchObject({ code: "DEPLOY_FAILED", summary: "Custom domain remove failed", }); }); it("domain add rejects preview branches", async () => { - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainAdd } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -917,11 +1082,13 @@ describe("app controller", () => { }, }); - await expect(runAppDomainAdd(context, "shop.acme.com", { - projectRef: "proj_123", - appName: "shop", - branchName: "feat/login", - })).rejects.toMatchObject({ + await expect( + runAppDomainAdd(context, "shop.acme.com", { + projectRef: "proj_123", + appName: "shop", + branchName: "feat/login", + }), + ).rejects.toMatchObject({ code: "BRANCH_NOT_DEPLOYABLE", domain: "branch", exitCode: 2, @@ -931,34 +1098,49 @@ describe("app controller", () => { it("domain retry maps API 409 to DOMAIN_RETRY_NOT_ELIGIBLE", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "shop", region: "eu-central-1", liveDeploymentId: "dep_live", liveUrl: "https://shop.prisma.app" }, - ]); - const listDomains = vi.fn().mockResolvedValue([ - createDomain({ status: "provisioning_tls" }), + { + id: "app_1", + name: "shop", + region: "eu-central-1", + liveDeploymentId: "dep_live", + liveUrl: "https://shop.prisma.app", + }, ]); + const listDomains = vi + .fn() + .mockResolvedValue([createDomain({ status: "provisioning_tls" })]); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", async (importOriginal) => { - const actual = await importOriginal(); - const retryDomain = vi.fn().mockRejectedValue(new actual.PreviewDomainApiError({ - summary: "Failed to retry custom domain", - status: 409, - message: "Domain is not eligible for retry.", - })); + const actual = + await importOriginal< + typeof import("../src/lib/app/preview-provider") + >(); + const retryDomain = vi.fn().mockRejectedValue( + new actual.PreviewDomainApiError({ + summary: "Failed to retry custom domain", + status: 409, + message: "Domain is not eligible for retry.", + }), + ); return { ...actual, - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - listDomains, - retryDomain, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + listDomains, + retryDomain, + }), + ), }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainRetry } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -971,10 +1153,12 @@ describe("app controller", () => { }, }); - await expect(runAppDomainRetry(context, "shop.acme.com", { - projectRef: "proj_123", - appName: "shop", - })).rejects.toMatchObject({ + await expect( + runAppDomainRetry(context, "shop.acme.com", { + projectRef: "proj_123", + appName: "shop", + }), + ).rejects.toMatchObject({ code: "DOMAIN_RETRY_NOT_ELIGIBLE", domain: "app", }); @@ -983,30 +1167,43 @@ describe("app controller", () => { it("domain wait supports poll-once timeout mode", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "shop", region: "eu-central-1", liveDeploymentId: "dep_live", liveUrl: "https://shop.prisma.app" }, - ]); - const listDomains = vi.fn().mockResolvedValue([ - createDomain({ status: "verifying" }), + { + id: "app_1", + name: "shop", + region: "eu-central-1", + liveDeploymentId: "dep_live", + liveUrl: "https://shop.prisma.app", + }, ]); + const listDomains = vi + .fn() + .mockResolvedValue([createDomain({ status: "verifying" })]); const showDomain = vi.fn(); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", async (importOriginal) => { - const actual = await importOriginal(); + const actual = + await importOriginal< + typeof import("../src/lib/app/preview-provider") + >(); return { ...actual, - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - listDomains, - showDomain, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + listDomains, + showDomain, + }), + ), }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDomainWait } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -1022,18 +1219,20 @@ describe("app controller", () => { }, }); - await expect(runAppDomainWait(context, "shop.acme.com", { - projectRef: "proj_123", - appName: "shop", - timeout: "0", - })).rejects.toMatchObject({ + await expect( + runAppDomainWait(context, "shop.acme.com", { + projectRef: "proj_123", + appName: "shop", + timeout: "0", + }), + ).rejects.toMatchObject({ code: "DOMAIN_VERIFICATION_TIMEOUT", domain: "app", exitCode: 1, }); expect(showDomain).not.toHaveBeenCalled(); - expect(stdout.buffer).toContain("\"command\":\"app.domain.wait\""); - expect(stdout.buffer).toContain("\"status\":\"verifying\""); + expect(stdout.buffer).toContain('"command":"app.domain.wait"'); + expect(stdout.buffer).toContain('"status":"verifying"'); }); it("uses an explicit project, branch, app, framework, and runtime for a first deploy", async () => { @@ -1079,36 +1278,42 @@ describe("app controller", () => { }; const requireComputeAuth = vi.fn().mockResolvedValue(client); const listApps = vi.fn().mockResolvedValue([]); - const deployApp = vi.fn().mockImplementation(async (options: { appName?: string }) => ({ - projectId: "proj_my_app", - app: { - id: "app_new", - name: options.appName ?? "my-app", - region: "eu-central-1", - liveDeploymentId: "dep_123", - }, - deployment: { - id: "dep_123", - status: "running", - url: "https://my-app.prisma.app", - }, - })); + const deployApp = vi + .fn() + .mockImplementation(async (options: { appName?: string }) => ({ + projectId: "proj_my_app", + app: { + id: "app_new", + name: options.appName ?? "my-app", + region: "eu-central-1", + liveDeploymentId: "dep_123", + }, + deployment: { + id: "dep_123", + status: "running", + url: "https://my-app.prisma.app", + }, + })); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writePackageJson(cwd, { @@ -1190,7 +1395,9 @@ describe("app controller", () => { written: true, }, }); - expect(stderr.buffer).toContain(`Linked "./${path.basename(cwd)}" to Project "my-app"`); + expect(stderr.buffer).toContain( + `Linked "./${path.basename(cwd)}" to Project "my-app"`, + ); expect(stderr.buffer).toContain("Saved .prisma/local.json"); expect(stderr.buffer).toContain("Deploying to my-app / feat-j1 / my-app"); expect(stderr.buffer).toContain("Created prisma.app.json"); @@ -1207,26 +1414,34 @@ describe("app controller", () => { workspaceId: "ws_123", projectId: "proj_my_app", }); - await expect(readFile(path.join(cwd, ".gitignore"), "utf8")).resolves.toBe(".prisma/\n"); + await expect(readFile(path.join(cwd, ".gitignore"), "utf8")).resolves.toBe( + ".prisma/\n", + ); }); it("returns LOCAL_STATE_WRITE_FAILED when deploy cannot store the local binding", async () => { - const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient("proj_my_app")); + const requireComputeAuth = vi + .fn() + .mockResolvedValue(createProjectClient("proj_my_app")); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps: vi.fn(), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps: vi.fn(), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await mkdir(path.join(cwd, ".gitignore"), { recursive: true }); @@ -1241,9 +1456,11 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, undefined, { - projectRef: "proj_my_app", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, undefined, { + projectRef: "proj_my_app", + }), + ).rejects.toMatchObject({ code: "LOCAL_STATE_WRITE_FAILED", domain: "project", meta: { @@ -1260,7 +1477,13 @@ describe("app controller", () => { it("uses existing prisma.app.json deploy settings", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const deployApp = vi.fn().mockResolvedValue({ projectId: "proj_123", @@ -1292,7 +1515,9 @@ describe("app controller", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writePackageJson(cwd, { @@ -1303,11 +1528,15 @@ describe("app controller", () => { }); await writeFile( path.join(cwd, "prisma.app.json"), - `${JSON.stringify({ - $schema: "https://pris.ly/schemas/prisma-app-config.v1.json", - buildCommand: "bun run build", - outputDirectory: ".next/standalone", - }, null, 2)}\n`, + `${JSON.stringify( + { + $schema: "https://pris.ly/schemas/prisma-app-config.v1.json", + buildCommand: "bun run build", + outputDirectory: ".next/standalone", + }, + null, + 2, + )}\n`, "utf8", ); const stateDir = path.join(cwd, ".state"); @@ -1361,26 +1590,34 @@ describe("app controller", () => { it("writes the local binding before build failures and renders build-failure copy", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([]); - const deployApp = vi.fn().mockImplementation(async (options: { progress?: { onBuildStart?: () => void } }) => { - options.progress?.onBuildStart?.(); - throw new Error("next build exited with code 1"); - }); + const deployApp = vi + .fn() + .mockImplementation( + async (options: { progress?: { onBuildStart?: () => void } }) => { + options.progress?.onBuildStart?.(); + throw new Error("next build exited with code 1"); + }, + ); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), - })); + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), + })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -1394,10 +1631,12 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - projectRef: "proj_123", - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + framework: "hono", + }), + ).rejects.toMatchObject({ code: "BUILD_FAILED", humanLines: [ "Build failed locally.", @@ -1411,7 +1650,9 @@ describe("app controller", () => { workspaceId: "ws_123", projectId: "proj_123", }); - expect(stderr.buffer).toContain(`Linked "./${path.basename(cwd)}" to Project "Acme Dashboard"`); + expect(stderr.buffer).toContain( + `Linked "./${path.basename(cwd)}" to Project "Acme Dashboard"`, + ); expect(stderr.buffer).toContain("Saved .prisma/local.json"); expect(stderr.buffer).toContain("Building locally..."); }); @@ -1419,26 +1660,36 @@ describe("app controller", () => { it("surfaces a concrete Next.js standalone-output recovery action", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([]); - const deployApp = vi.fn().mockImplementation(async (options: { progress?: { onBuildStart?: () => void } }) => { - options.progress?.onBuildStart?.(); - throw new Error('Next.js build did not produce standalone output. Add output: "standalone" to your next.config file.'); - }); + const deployApp = vi + .fn() + .mockImplementation( + async (options: { progress?: { onBuildStart?: () => void } }) => { + options.progress?.onBuildStart?.(); + throw new Error( + 'Next.js build did not produce standalone output. Add output: "standalone" to your next.config file.', + ); + }, + ); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -1452,20 +1703,24 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - projectRef: "proj_123", - framework: "nextjs", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + framework: "nextjs", + }), + ).rejects.toMatchObject({ code: "BUILD_FAILED", - fix: "Add output: \"standalone\" to next.config.*, then rerun deploy.", + fix: 'Add output: "standalone" to next.config.*, then rerun deploy.', humanLines: [ "Build failed locally.", "", - "✗ Built Next.js build did not produce standalone output. Add output: \"standalone\" to your next.config file.", + '✗ Built Next.js build did not produce standalone output. Add output: "standalone" to your next.config file.', "", - "Fix: Add output: \"standalone\" to next.config.*, then rerun deploy.", + 'Fix: Add output: "standalone" to next.config.*, then rerun deploy.', + ], + nextSteps: [ + 'Add output: "standalone" to next.config.*, then rerun prisma-cli app deploy', ], - nextSteps: ["Add output: \"standalone\" to next.config.*, then rerun prisma-cli app deploy"], nextActions: [ expect.objectContaining({ kind: "edit-file", @@ -1484,47 +1739,59 @@ describe("app controller", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); let appName = ""; const listApps = vi.fn().mockImplementation(async () => [ - { id: "app_1", name: appName, region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: appName, + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); - const deployApp = vi.fn().mockImplementation(async (options: { - progress?: { - onBuildStart?: () => void; - onBuildComplete?: () => void; - onArchiveCreating?: () => void; - onArchiveReady?: (byteLength: number) => void; - onUploadStart?: () => void; - onVersionCreated?: (versionId: string) => void; - onUploadComplete?: () => void; - onStartRequested?: () => void; - onRunning?: (url?: string) => void; - }; - }) => { - options.progress?.onBuildStart?.(); - options.progress?.onBuildComplete?.(); - options.progress?.onArchiveCreating?.(); - options.progress?.onArchiveReady?.(11_114_905); - options.progress?.onUploadStart?.(); - options.progress?.onVersionCreated?.("dep_failed"); - options.progress?.onUploadComplete?.(); - options.progress?.onStartRequested?.(); - options.progress?.onRunning?.("https://cv-example.fra.prisma.build"); - throw new Error("Internal Server Error"); - }); + const deployApp = vi.fn().mockImplementation( + async (options: { + progress?: { + onBuildStart?: () => void; + onBuildComplete?: () => void; + onArchiveCreating?: () => void; + onArchiveReady?: (byteLength: number) => void; + onUploadStart?: () => void; + onVersionCreated?: (versionId: string) => void; + onUploadComplete?: () => void; + onStartRequested?: () => void; + onRunning?: (url?: string) => void; + }; + }) => { + options.progress?.onBuildStart?.(); + options.progress?.onBuildComplete?.(); + options.progress?.onArchiveCreating?.(); + options.progress?.onArchiveReady?.(11_114_905); + options.progress?.onUploadStart?.(); + options.progress?.onVersionCreated?.("dep_failed"); + options.progress?.onUploadComplete?.(); + options.progress?.onStartRequested?.(); + options.progress?.onRunning?.("https://cv-example.fra.prisma.build"); + throw new Error("Internal Server Error"); + }, + ); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); appName = path.basename(cwd); @@ -1543,9 +1810,11 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, undefined, { - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, undefined, { + framework: "hono", + }), + ).rejects.toMatchObject({ code: "DEPLOY_FAILED", humanLines: expect.arrayContaining([ "The deployment started, but the app is not ready yet.", @@ -1557,7 +1826,9 @@ describe("app controller", () => { "https://cv-example.fra.prisma.build", ]), }); - expect(stderr.buffer).toContain(`Deploying ./${path.basename(cwd)} to Acme Dashboard / main / ${path.basename(cwd)}`); + expect(stderr.buffer).toContain( + `Deploying ./${path.basename(cwd)} to Acme Dashboard / main / ${path.basename(cwd)}`, + ); expect(stderr.buffer).toContain(" Built 10.6 MB"); expect(stderr.buffer).toContain(" Uploaded"); expect(stderr.buffer).toContain("Deploying..."); @@ -1571,39 +1842,51 @@ describe("app controller", () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); let appName = ""; const listApps = vi.fn().mockImplementation(async () => [ - { id: "app_1", name: appName, region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: appName, + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); - const deployApp = vi.fn().mockImplementation(async (options: { - progress?: { - onBuildStart?: () => void; - onBuildComplete?: () => void; - onArchiveCreating?: () => void; - onArchiveReady?: (byteLength: number) => void; - onUploadStart?: () => void; - }; - }) => { - options.progress?.onBuildStart?.(); - options.progress?.onBuildComplete?.(); - options.progress?.onArchiveCreating?.(); - options.progress?.onArchiveReady?.(11_114_905); - options.progress?.onUploadStart?.(); - throw new Error("Upload failed"); - }); + const deployApp = vi.fn().mockImplementation( + async (options: { + progress?: { + onBuildStart?: () => void; + onBuildComplete?: () => void; + onArchiveCreating?: () => void; + onArchiveReady?: (byteLength: number) => void; + onUploadStart?: () => void; + }; + }) => { + options.progress?.onBuildStart?.(); + options.progress?.onBuildComplete?.(); + options.progress?.onArchiveCreating?.(); + options.progress?.onArchiveReady?.(11_114_905); + options.progress?.onUploadStart?.(); + throw new Error("Upload failed"); + }, + ); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); appName = path.basename(cwd); @@ -1622,9 +1905,11 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, undefined, { - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, undefined, { + framework: "hono", + }), + ).rejects.toMatchObject({ code: "DEPLOY_FAILED", summary: "Deploy failed after the build completed.", humanLines: expect.arrayContaining([ @@ -1644,13 +1929,15 @@ describe("app controller", () => { }, { name: "Next.js from next.config.mts", - files: { "next.config.mts": "export default { output: \"standalone\" }\n" }, + files: { "next.config.mts": 'export default { output: "standalone" }\n' }, expectedBuildType: "nextjs", }, { name: "Hono from package.json", packageJson: { dependencies: { hono: "4.0.0" } }, - files: { "src/index.ts": "export default { fetch: () => new Response('ok') }\n" }, + files: { + "src/index.ts": "export default { fetch: () => new Response('ok') }\n", + }, expectedEntrypoint: "src/index.ts", expectedBuildType: "bun", }, @@ -1682,112 +1969,132 @@ describe("app controller", () => { packageJson: { dependencies: { "@tanstack/solid-start": "1.0.0" } }, expectedBuildType: "tanstack-start", }, - ])( - "detects deploy framework: $name", - async ({ packageJson, files, framework, entrypoint, expectedEntrypoint, expectedBuildType }) => { - const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); - const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, - ]); - const deployApp = vi.fn().mockResolvedValue({ - projectId: "proj_123", - app: { - id: "app_1", - name: "hello-world", - region: "eu-central-1", - liveDeploymentId: "dep_123", - liveUrl: "https://hello-world.prisma.app", - }, - deployment: { - id: "dep_123", - status: "running", - url: "https://hello-world.prisma.app", - }, - }); + ])("detects deploy framework: $name", async ({ + packageJson, + files, + framework, + entrypoint, + expectedEntrypoint, + expectedBuildType, + }) => { + const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); + const listApps = vi.fn().mockResolvedValue([ + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, + ]); + const deployApp = vi.fn().mockResolvedValue({ + projectId: "proj_123", + app: { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_123", + liveUrl: "https://hello-world.prisma.app", + }, + deployment: { + id: "dep_123", + status: "running", + url: "https://hello-world.prisma.app", + }, + }); - vi.doMock("../src/lib/auth/guard", () => ({ - requireComputeAuth, - })); - vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ + vi.doMock("../src/lib/auth/guard", () => ({ + requireComputeAuth, + })); + vi.doMock("../src/lib/app/preview-provider", () => ({ + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ resolveBranch: createResolveBranch(), listApps, deployApp, listDeployments: vi.fn(), showDeployment: vi.fn(), - })), - })); + }), + ), + })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); - const { runAppDeploy } = await import("../src/controllers/app"); - const cwd = await createTempCwd(); - if (packageJson) { - await writePackageJson(cwd, packageJson); - } - for (const [fileName, content] of Object.entries(files ?? {})) { - const filePath = path.join(cwd, fileName); - await mkdir(path.dirname(filePath), { recursive: true }); - await writeFile(filePath, content); - } - const stateDir = path.join(cwd, ".state"); - const { context } = await createTestCommandContext({ - cwd, - stateDir, - env: { - ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, - }, - }); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); + const { runAppDeploy } = await import("../src/controllers/app"); + const cwd = await createTempCwd(); + if (packageJson) { + await writePackageJson(cwd, packageJson); + } + for (const [fileName, content] of Object.entries(files ?? {})) { + const filePath = path.join(cwd, fileName); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, content); + } + const stateDir = path.join(cwd, ".state"); + const { context } = await createTestCommandContext({ + cwd, + stateDir, + env: { + ...process.env, + PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, + }, + }); - await runAppDeploy(context, "hello-world", { - projectRef: "proj_123", - framework, - entrypoint, - }); + await runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + framework, + entrypoint, + }); - expect(deployApp).toHaveBeenCalledWith( - expect.objectContaining({ - entrypoint: expectedEntrypoint ?? entrypoint, - buildType: expectedBuildType, - portMapping: { http: 3000 }, - }), - ); - }, - ); + expect(deployApp).toHaveBeenCalledWith( + expect.objectContaining({ + entrypoint: expectedEntrypoint ?? entrypoint, + buildType: expectedBuildType, + portMapping: { http: 3000 }, + }), + ); + }); it("lets PRISMA_PROJECT_ID skip the local pin and resolve the project", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([]); - const deployApp = vi.fn().mockImplementation(async (options: { appName?: string }) => ({ - projectId: "proj_123", - app: { - id: "app_env", - name: options.appName ?? "env-app", - region: "eu-central-1", - liveDeploymentId: "dep_123", - liveUrl: "https://env-app.prisma.app", - }, - deployment: { - id: "dep_123", - status: "running", - url: "https://env-app.prisma.app", - }, - })); + const deployApp = vi + .fn() + .mockImplementation(async (options: { appName?: string }) => ({ + projectId: "proj_123", + app: { + id: "app_env", + name: options.appName ?? "env-app", + region: "eu-central-1", + liveDeploymentId: "dep_123", + liveUrl: "https://env-app.prisma.app", + }, + deployment: { + id: "dep_123", + status: "running", + url: "https://env-app.prisma.app", + }, + })); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { @@ -1823,7 +2130,9 @@ describe("app controller", () => { workspaceId: "ws_123", projectId: "proj_stale", }); - expect(stderr.buffer).toContain(`Deploying ./${path.basename(cwd)} to Acme Dashboard / main / ${path.basename(cwd)}`); + expect(stderr.buffer).toContain( + `Deploying ./${path.basename(cwd)} to Acme Dashboard / main / ${path.basename(cwd)}`, + ); expect(stderr.buffer).not.toContain("from PRISMA_PROJECT_ID"); }); @@ -1837,17 +2146,21 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject, - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject, + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -1866,15 +2179,15 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + framework: "hono", + }), + ).rejects.toMatchObject({ code: "PROJECT_SETUP_REQUIRED", domain: "project", meta: { - candidates: [ - { id: "proj_123", name: "Acme Dashboard" }, - ], + candidates: [{ id: "proj_123", name: "Acme Dashboard" }], suggestedProjectName: path.basename(cwd), suggestedProjectNameSource: "directory-name", recoveryCommands: expect.arrayContaining([ @@ -1904,12 +2217,18 @@ describe("app controller", () => { expect(createProject).not.toHaveBeenCalled(); expect(listApps).not.toHaveBeenCalled(); expect(deployApp).not.toHaveBeenCalled(); - await expect(readFile(path.join(cwd, ".prisma/local.json"), "utf8")).rejects.toMatchObject({ code: "ENOENT" }); - await expect(readFile(path.join(cwd, ".gitignore"), "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + readFile(path.join(cwd, ".gitignore"), "utf8"), + ).rejects.toMatchObject({ code: "ENOENT" }); }); it("rejects mutually exclusive Project sources before resolving deploy context", async () => { - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -1923,11 +2242,13 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - projectRef: "proj_123", - createProjectName: "new-project", - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + createProjectName: "new-project", + framework: "hono", + }), + ).rejects.toMatchObject({ code: "USAGE_ERROR", domain: "project", summary: "Project selection is ambiguous", @@ -1945,10 +2266,12 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(envContext, "hello-world", { - projectRef: "proj_456", - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(envContext, "hello-world", { + projectRef: "proj_456", + framework: "hono", + }), + ).rejects.toMatchObject({ code: "USAGE_ERROR", domain: "project", summary: "Project selection is ambiguous", @@ -1979,17 +2302,21 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject, - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject, + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2020,7 +2347,9 @@ describe("app controller", () => { projectId: "proj_123", }); expect(stderr.buffer).toContain("Which Project should this directory use?"); - expect(stderr.buffer).toContain(`Linked "./${path.basename(cwd)}" to Project "Acme Dashboard"`); + expect(stderr.buffer).toContain( + `Linked "./${path.basename(cwd)}" to Project "Acme Dashboard"`, + ); }); it("interactive first deploy previews detected framework and runtime before the customization prompt", async () => { @@ -2046,21 +2375,25 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - createProject, - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), - })); - - const { createTempCwd, createTestCommandContext } = await import("./helpers"); - const { runAppDeploy } = await import("../src/controllers/app"); - const cwd = await createTempCwd(); - await writePackageJson(cwd, { - name: "hello-world", + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + createProject, + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), + })); + + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); + const { runAppDeploy } = await import("../src/controllers/app"); + const cwd = await createTempCwd(); + await writePackageJson(cwd, { + name: "hello-world", dependencies: { next: "15.0.0", }, @@ -2087,7 +2420,9 @@ describe("app controller", () => { }), ); - const targetIndex = stderr.buffer.indexOf("Deploying to Acme Dashboard / main / hello-world"); + const targetIndex = stderr.buffer.indexOf( + "Deploying to Acme Dashboard / main / hello-world", + ); const detectedIndex = stderr.buffer.indexOf("Detected Next.js"); const promptIndex = stderr.buffer.indexOf("Customize build settings?"); @@ -2128,17 +2463,21 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject, - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject, + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writePackageJson(cwd, { @@ -2195,16 +2534,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps: vi.fn(), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps: vi.fn(), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2217,9 +2560,11 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - projectRef: "proj_123", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + }), + ).rejects.toMatchObject({ code: "FRAMEWORK_NOT_DETECTED", domain: "app", exitCode: 2, @@ -2235,16 +2580,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { @@ -2262,9 +2611,11 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, undefined, { - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, undefined, { + framework: "hono", + }), + ).rejects.toMatchObject({ code: "LOCAL_STATE_STALE", domain: "project", meta: { @@ -2285,16 +2636,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { @@ -2313,9 +2668,11 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, undefined, { - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, undefined, { + framework: "hono", + }), + ).rejects.toMatchObject({ code: "LOCAL_STATE_STALE", meta: { pinPath: ".prisma/local.json", @@ -2326,7 +2683,9 @@ describe("app controller", () => { }); it("returns LOCAL_STATE_STALE when the local pin cannot be parsed", async () => { - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, "{ nope"); @@ -2341,9 +2700,11 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, undefined, { - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, undefined, { + framework: "hono", + }), + ).rejects.toMatchObject({ code: "LOCAL_STATE_STALE", meta: { pinPath: ".prisma/local.json", @@ -2354,8 +2715,20 @@ describe("app controller", () => { it("returns APP_AMBIGUOUS for duplicate app names in non-interactive mode", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, - { id: "app_2", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, + { + id: "app_2", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const deployApp = vi.fn(); @@ -2363,16 +2736,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2386,10 +2763,12 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - projectRef: "proj_123", - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + framework: "hono", + }), + ).rejects.toMatchObject({ code: "APP_AMBIGUOUS", domain: "app", exitCode: 2, @@ -2404,7 +2783,9 @@ describe("app controller", () => { }); it("rejects --entry together with --framework nextjs for deploy", async () => { - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2417,10 +2798,12 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - framework: "nextjs", - entrypoint: "server.js", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + framework: "nextjs", + entrypoint: "server.js", + }), + ).rejects.toMatchObject({ code: "USAGE_ERROR", domain: "app", summary: "App deploy does not accept --entry with Next.js", @@ -2428,7 +2811,9 @@ describe("app controller", () => { }); it("rejects invalid --http-port values for deploy", async () => { - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2441,9 +2826,11 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - httpPort: "70000", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + httpPort: "70000", + }), + ).rejects.toMatchObject({ code: "USAGE_ERROR", domain: "app", summary: 'Invalid HTTP port "70000"', @@ -2453,35 +2840,41 @@ describe("app controller", () => { it("interactive first deploy can create a new app when none is selected", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([]); - const deployApp = vi.fn().mockImplementation(async (options: { appName?: string }) => ({ - projectId: "proj_123", - app: { - id: "app_new", - name: options.appName ?? "hello-world", - region: "eu-west-3", - liveDeploymentId: "dep_123", - }, - deployment: { - id: "dep_123", - status: "running", - url: "https://hello-world.prisma.app", - }, - })); + const deployApp = vi + .fn() + .mockImplementation(async (options: { appName?: string }) => ({ + projectId: "proj_123", + app: { + id: "app_new", + name: options.appName ?? "hello-world", + region: "eu-west-3", + liveDeploymentId: "dep_123", + }, + deployment: { + id: "dep_123", + status: "running", + url: "https://hello-world.prisma.app", + }, + })); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2517,35 +2910,41 @@ describe("app controller", () => { it("auto-creates the inferred app without prompting in non-interactive mode", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([]); - const deployApp = vi.fn().mockImplementation(async (options: { appName?: string }) => ({ - projectId: "proj_123", - app: { - id: "app_new", - name: options.appName ?? "created-app", - region: "eu-central-1", - liveDeploymentId: "dep_123", - }, - deployment: { - id: "dep_123", - status: "running", - url: "https://created-app.prisma.app", - }, - })); + const deployApp = vi + .fn() + .mockImplementation(async (options: { appName?: string }) => ({ + projectId: "proj_123", + app: { + id: "app_new", + name: options.appName ?? "created-app", + region: "eu-central-1", + liveDeploymentId: "dep_123", + }, + deployment: { + id: "dep_123", + status: "running", + url: "https://created-app.prisma.app", + }, + })); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2594,16 +2993,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2659,17 +3062,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject, - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject, + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext, readPrismaConfig } = await import("./helpers"); + const { createTempCwd, createTestCommandContext, readPrismaConfig } = + await import("./helpers"); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2699,8 +3105,12 @@ describe("app controller", () => { appName: "hello-world", }), ); - await expect(readPrismaConfig(cwd)).rejects.toMatchObject({ code: "ENOENT" }); - await expect(context.stateStore.readSelectedApp("proj_new")).resolves.toEqual({ + await expect(readPrismaConfig(cwd)).rejects.toMatchObject({ + code: "ENOENT", + }); + await expect( + context.stateStore.readSelectedApp("proj_new"), + ).resolves.toEqual({ id: "app_new", name: "hello-world", }); @@ -2723,7 +3133,9 @@ describe("app controller", () => { workspaceId: "ws_123", projectId: "proj_new", }); - await expect(readFile(path.join(cwd, ".gitignore"), "utf8")).resolves.toBe(".prisma/\n"); + await expect(readFile(path.join(cwd, ".gitignore"), "utf8")).resolves.toBe( + ".prisma/\n", + ); }); it("reuses the created project on second deploy instead of creating another one", async () => { @@ -2737,7 +3149,12 @@ describe("app controller", () => { .fn() .mockResolvedValueOnce([]) .mockResolvedValueOnce([ - { id: "app_new", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_123" }, + { + id: "app_new", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_123", + }, ]); const deployApp = vi .fn() @@ -2774,17 +3191,21 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject, - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject, + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2812,52 +3233,59 @@ describe("app controller", () => { projectId: "proj_new", }); stderr.buffer = ""; - client.GET.mockImplementation((pathName: string, request?: { params?: { query?: { gitName?: string } } }) => { - if (pathName === "/v1/projects") { - return { - data: { - data: [ - { - id: "proj_new", - name: path.basename(cwd), - slug: path.basename(cwd), - workspace: { - id: "ws_123", - name: "Acme Inc", + client.GET.mockImplementation( + ( + pathName: string, + request?: { params?: { query?: { gitName?: string } } }, + ) => { + if (pathName === "/v1/projects") { + return { + data: { + data: [ + { + id: "proj_new", + name: path.basename(cwd), + slug: path.basename(cwd), + workspace: { + id: "ws_123", + name: "Acme Inc", + }, }, - }, - ], - }, - }; - } - - if (pathName === "/v1/projects/{projectId}/branches") { - const branchName = request?.params?.query?.gitName ?? "main"; - return { - data: { - data: [ - { - id: `branch_${branchName.replace(/[^a-z0-9]+/gi, "_")}`, - gitName: branchName, - isDefault: branchName === "main", - role: "preview", - }, - ], - }, - }; - } - - throw new Error(`Unexpected path ${pathName}`); - }); - const secondResult = await runAppDeploy(context, "hello-world", { - framework: "hono", - }); + ], + }, + }; + } - expect(createProject).toHaveBeenCalledTimes(1); - expect(secondResult.result.localPin).toBeUndefined(); + if (pathName === "/v1/projects/{projectId}/branches") { + const branchName = request?.params?.query?.gitName ?? "main"; + return { + data: { + data: [ + { + id: `branch_${branchName.replace(/[^a-z0-9]+/gi, "_")}`, + gitName: branchName, + isDefault: branchName === "main", + role: "preview", + }, + ], + }, + }; + } + + throw new Error(`Unexpected path ${pathName}`); + }, + ); + const secondResult = await runAppDeploy(context, "hello-world", { + framework: "hono", + }); + + expect(createProject).toHaveBeenCalledTimes(1); + expect(secondResult.result.localPin).toBeUndefined(); expect(stderr.buffer).toContain(`Deploying ./${path.basename(cwd)}`); expect(stderr.buffer).not.toContain("Set up"); - await expect(readFile(path.join(cwd, ".gitignore"), "utf8")).resolves.toBe(".prisma/\n"); + await expect(readFile(path.join(cwd, ".gitignore"), "utf8")).resolves.toBe( + ".prisma/\n", + ); expect(deployApp).toHaveBeenNthCalledWith( 2, expect.objectContaining({ @@ -2892,17 +3320,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject, - listApps: vi.fn().mockResolvedValue([]), - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject, + listApps: vi.fn().mockResolvedValue([]), + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext, readPrismaConfig } = await import("./helpers"); + const { createTempCwd, createTestCommandContext, readPrismaConfig } = + await import("./helpers"); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2917,10 +3348,12 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - createProjectName: "next-smoke", - framework: "hono", - })).resolves.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + createProjectName: "next-smoke", + framework: "hono", + }), + ).resolves.toMatchObject({ result: { project: { id: "proj_new", @@ -2936,28 +3369,36 @@ describe("app controller", () => { name: "next-smoke", signal: context.runtime.signal, }); - await expect(readPrismaConfig(cwd)).rejects.toMatchObject({ code: "ENOENT" }); + await expect(readPrismaConfig(cwd)).rejects.toMatchObject({ + code: "ENOENT", + }); }); it("returns PROJECT_CREATE_FAILED when explicit deploy-time project creation is rejected with 401", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); - const createProject = vi.fn().mockRejectedValue(new Error("Authentication failed (HTTP 401)")); + const createProject = vi + .fn() + .mockRejectedValue(new Error("Authentication failed (HTTP 401)")); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject, - listApps: vi.fn().mockResolvedValue([]), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject, + listApps: vi.fn().mockResolvedValue([]), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -2972,38 +3413,48 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - createProjectName: "next-smoke", - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + createProjectName: "next-smoke", + framework: "hono", + }), + ).rejects.toMatchObject({ code: "PROJECT_CREATE_FAILED", domain: "project", summary: 'Could not create Project "next-smoke"', why: expect.stringContaining("HTTP 401"), fix: expect.stringContaining("--project"), - nextSteps: expect.arrayContaining(["prisma-cli app deploy --project "]), + nextSteps: expect.arrayContaining([ + "prisma-cli app deploy --project ", + ]), }); }); it("returns PROJECT_CREATE_FAILED when explicit deploy-time project creation fails", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); - const createProject = vi.fn().mockRejectedValue(new Error("Internal Server Error (HTTP 503)")); + const createProject = vi + .fn() + .mockRejectedValue(new Error("Internal Server Error (HTTP 503)")); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject, - listApps: vi.fn().mockResolvedValue([]), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject, + listApps: vi.fn().mockResolvedValue([]), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -3018,23 +3469,32 @@ describe("app controller", () => { }, }); - await expect(runAppDeploy(context, "hello-world", { - createProjectName: "next-smoke", - framework: "hono", - })).rejects.toMatchObject({ + await expect( + runAppDeploy(context, "hello-world", { + createProjectName: "next-smoke", + framework: "hono", + }), + ).rejects.toMatchObject({ code: "PROJECT_CREATE_FAILED", domain: "project", summary: 'Could not create Project "next-smoke"', why: expect.stringContaining("Internal Server Error"), fix: expect.stringContaining("--project"), - nextSteps: expect.arrayContaining(["prisma-cli app deploy --project "]), + nextSteps: expect.arrayContaining([ + "prisma-cli app deploy --project ", + ]), }); }); it("does not use saved app selection as the deploy target source", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: "dep_live" }, + { + id: "app_1", + name: "hello-world", + region: "eu-west-3", + liveDeploymentId: "dep_live", + }, ]); const deployApp = vi.fn().mockResolvedValue({ projectId: "proj_123", @@ -3055,16 +3515,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -3098,7 +3562,12 @@ describe("app controller", () => { it("list-deploys sorts deployments newest first for the selected app", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: "dep_2" }, + { + id: "app_1", + name: "hello-world", + region: "eu-west-3", + liveDeploymentId: "dep_2", + }, ]); const listDeployments = vi.fn().mockResolvedValue({ app: { @@ -3129,16 +3598,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp: vi.fn(), - listDeployments, - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp: vi.fn(), + listDeployments, + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppListDeploys } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -3154,7 +3627,9 @@ describe("app controller", () => { const result = await runAppListDeploys(context, "hello-world"); - expect(result.result.deployments.map((deployment) => deployment.id)).toEqual(["dep_2", "dep_1"]); + expect( + result.result.deployments.map((deployment) => deployment.id), + ).toEqual(["dep_2", "dep_1"]); }); it("returns PROJECT_NOT_FOUND when the resolved project is not accessible in real mode", async () => { @@ -3165,15 +3640,19 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppListDeploys } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -3197,7 +3676,12 @@ describe("app controller", () => { it("list-deploys uses the local known live deployment when the provider cannot confirm it", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-west-3", + liveDeploymentId: null, + }, ]); const listDeployments = vi.fn().mockResolvedValue({ app: { @@ -3228,16 +3712,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp: vi.fn(), - listDeployments, - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp: vi.fn(), + listDeployments, + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppListDeploys } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -3255,7 +3743,11 @@ describe("app controller", () => { id: "app_1", name: "hello-world", }); - await context.stateStore.setKnownLiveDeployment("proj_123", "app_1", "dep_1"); + await context.stateStore.setKnownLiveDeployment( + "proj_123", + "app_1", + "dep_1", + ); const result = await runAppListDeploys(context, "hello-world"); @@ -3273,18 +3765,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - deployApp: vi.fn(), - listDeployments: vi.fn(), - promoteDeployment: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + deployApp: vi.fn(), + listDeployments: vi.fn(), + promoteDeployment: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppShow } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writePackageJson(cwd, { name: "acme-dashboard" }); @@ -3330,18 +3826,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - deployApp: vi.fn(), - listDeployments: vi.fn(), - promoteDeployment: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + deployApp: vi.fn(), + listDeployments: vi.fn(), + promoteDeployment: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppShow } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -3408,18 +3908,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - deployApp: vi.fn(), - listDeployments, - promoteDeployment: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + deployApp: vi.fn(), + listDeployments, + promoteDeployment: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppShow } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -3510,18 +4014,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - deployApp: vi.fn(), - listDeployments, - promoteDeployment: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + deployApp: vi.fn(), + listDeployments, + promoteDeployment: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppShow } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -3535,13 +4043,25 @@ describe("app controller", () => { }, }); - await context.stateStore.setKnownLiveDeployment("proj_123", "app_1", "dep_2"); + await context.stateStore.setKnownLiveDeployment( + "proj_123", + "app_1", + "dep_2", + ); const result = await runAppShow(context, "hello-world"); expect(result.result.liveDeployment?.id).toBe("dep_2"); - expect(result.result.recentDeployments.find((deployment) => deployment.id === "dep_2")?.live).toBe(true); - expect(result.result.recentDeployments.find((deployment) => deployment.id === "dep_3")?.live).toBe(false); + expect( + result.result.recentDeployments.find( + (deployment) => deployment.id === "dep_2", + )?.live, + ).toBe(true); + expect( + result.result.recentDeployments.find( + (deployment) => deployment.id === "dep_3", + )?.live, + ).toBe(false); }); it("show-deploy returns deployment detail without branch inference", async () => { @@ -3566,16 +4086,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps: vi.fn(), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps: vi.fn(), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment, + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppShowDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -3627,16 +4151,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps: vi.fn(), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps: vi.fn(), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment, + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppShowDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -3649,7 +4177,11 @@ describe("app controller", () => { }, }); - await context.stateStore.setKnownLiveDeployment("proj_123", "app_1", "dep_123"); + await context.stateStore.setKnownLiveDeployment( + "proj_123", + "app_1", + "dep_123", + ); const result = await runAppShowDeploy(context, "dep_123"); @@ -3678,16 +4210,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps: vi.fn(), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps: vi.fn(), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment, + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppShowDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -3705,7 +4241,11 @@ describe("app controller", () => { name: "Other Project", workspaceId: "ws_other", }); - await context.stateStore.setKnownLiveDeployment("proj_other", "app_1", "dep_123"); + await context.stateStore.setKnownLiveDeployment( + "proj_other", + "app_1", + "dep_123", + ); const result = await runAppShowDeploy(context, "dep_123"); @@ -3714,22 +4254,28 @@ describe("app controller", () => { it("show-deploy surfaces provider failures instead of reporting not found", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); - const showDeployment = vi.fn().mockRejectedValue(new Error("Missing or invalid authorization token")); + const showDeployment = vi + .fn() + .mockRejectedValue(new Error("Missing or invalid authorization token")); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps: vi.fn(), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps: vi.fn(), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment, + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppShowDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -3787,18 +4333,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - deployApp: vi.fn(), - listDeployments, - promoteDeployment: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + deployApp: vi.fn(), + listDeployments, + promoteDeployment: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppOpen } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -3857,18 +4407,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - deployApp: vi.fn(), - listDeployments, - promoteDeployment: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + deployApp: vi.fn(), + listDeployments, + promoteDeployment: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppOpen } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -3926,18 +4480,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - deployApp: vi.fn(), - listDeployments, - promoteDeployment: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + deployApp: vi.fn(), + listDeployments, + promoteDeployment: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppOpen } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -3991,18 +4549,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - deployApp: vi.fn(), - listDeployments, - promoteDeployment: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + deployApp: vi.fn(), + listDeployments, + promoteDeployment: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppOpen } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4026,7 +4588,12 @@ describe("app controller", () => { it("promote switches the selected app to the requested deployment", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: "dep_2" }, + { + id: "app_1", + name: "hello-world", + region: "eu-west-3", + liveDeploymentId: "dep_2", + }, ]); const listDeployments = vi.fn().mockResolvedValue({ app: { @@ -4058,18 +4625,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - promoteDeployment, - deployApp: vi.fn(), - listDeployments, - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + promoteDeployment, + deployApp: vi.fn(), + listDeployments, + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppPromote } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4111,16 +4682,21 @@ describe("app controller", () => { it("promote returns a warning when the requested deployment is already live", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: "dep_2" }, - ]); - const listDeployments = vi.fn().mockResolvedValue({ - app: { + { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: "dep_2", }, - deployments: [ + ]); + const listDeployments = vi.fn().mockResolvedValue({ + app: { + id: "app_1", + name: "hello-world", + region: "eu-west-3", + liveDeploymentId: "dep_2", + }, + deployments: [ { id: "dep_2", status: "running", @@ -4136,18 +4712,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - promoteDeployment, - deployApp: vi.fn(), - listDeployments, - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + promoteDeployment, + deployApp: vi.fn(), + listDeployments, + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppPromote } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4164,13 +4744,20 @@ describe("app controller", () => { const result = await runAppPromote(context, "dep_2", "hello-world"); expect(promoteDeployment).not.toHaveBeenCalled(); - expect(result.warnings).toEqual(["The selected deployment is already live for this app."]); + expect(result.warnings).toEqual([ + "The selected deployment is already live for this app.", + ]); }); it("rollback chooses the previous deployment when no explicit target is provided", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: "dep_2" }, + { + id: "app_1", + name: "hello-world", + region: "eu-west-3", + liveDeploymentId: "dep_2", + }, ]); const listDeployments = vi.fn().mockResolvedValue({ app: { @@ -4202,18 +4789,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - promoteDeployment, - deployApp: vi.fn(), - listDeployments, - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + promoteDeployment, + deployApp: vi.fn(), + listDeployments, + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRollback } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4256,7 +4847,12 @@ describe("app controller", () => { it("rollback uses the local known live deployment when the provider cannot confirm it", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-west-3", + liveDeploymentId: null, + }, ]); const listDeployments = vi.fn().mockResolvedValue({ app: { @@ -4288,18 +4884,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - promoteDeployment, - deployApp: vi.fn(), - listDeployments, - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + promoteDeployment, + deployApp: vi.fn(), + listDeployments, + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRollback } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4313,7 +4913,11 @@ describe("app controller", () => { }, }); - await context.stateStore.setKnownLiveDeployment("proj_123", "app_1", "dep_1"); + await context.stateStore.setKnownLiveDeployment( + "proj_123", + "app_1", + "dep_1", + ); const result = await runAppRollback(context, "hello-world", undefined); @@ -4324,13 +4928,20 @@ describe("app controller", () => { }), ); expect(result.result.previousLiveDeploymentId).toBe("dep_1"); - await expect(context.stateStore.readKnownLiveDeployment("proj_123", "app_1")).resolves.toBe("dep_2"); + await expect( + context.stateStore.readKnownLiveDeployment("proj_123", "app_1"), + ).resolves.toBe("dep_2"); }); it("rollback uses an explicit deployment target when provided", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: "dep_3" }, + { + id: "app_1", + name: "hello-world", + region: "eu-west-3", + liveDeploymentId: "dep_3", + }, ]); const listDeployments = vi.fn().mockResolvedValue({ app: { @@ -4369,18 +4980,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - promoteDeployment, - deployApp: vi.fn(), - listDeployments, - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + promoteDeployment, + deployApp: vi.fn(), + listDeployments, + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRollback } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4408,7 +5023,12 @@ describe("app controller", () => { it("rollback returns NO_PREVIOUS_DEPLOYMENT when only one deployment exists", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-west-3", liveDeploymentId: "dep_2" }, + { + id: "app_1", + name: "hello-world", + region: "eu-west-3", + liveDeploymentId: "dep_2", + }, ]); const listDeployments = vi.fn().mockResolvedValue({ app: { @@ -4432,18 +5052,22 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - promoteDeployment: vi.fn(), - deployApp: vi.fn(), - listDeployments, - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + promoteDeployment: vi.fn(), + deployApp: vi.fn(), + listDeployments, + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRollback } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4457,44 +5081,54 @@ describe("app controller", () => { }, }); - await expect(runAppRollback(context, "hello-world", undefined)).rejects.toMatchObject({ + await expect( + runAppRollback(context, "hello-world", undefined), + ).rejects.toMatchObject({ code: "NO_PREVIOUS_DEPLOYMENT", domain: "app", }); }); it("does not reuse the wrong saved app when the resolved project changes", async () => { - const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient("proj_456")); + const requireComputeAuth = vi + .fn() + .mockResolvedValue(createProjectClient("proj_456")); const listApps = vi.fn().mockResolvedValue([]); - const deployApp = vi.fn().mockImplementation(async (options: { appName?: string }) => ({ - projectId: "proj_456", - app: { - id: "app_new", - name: options.appName ?? "created-app", - region: "eu-central-1", - liveDeploymentId: "dep_123", - }, - deployment: { - id: "dep_123", - status: "running", - url: "https://created-app.prisma.app", - }, - })); + const deployApp = vi + .fn() + .mockImplementation(async (options: { appName?: string }) => ({ + projectId: "proj_456", + app: { + id: "app_new", + name: options.appName ?? "created-app", + region: "eu-central-1", + liveDeploymentId: "dep_123", + }, + deployment: { + id: "dep_123", + status: "running", + url: "https://created-app.prisma.app", + }, + })); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - deployApp, - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + deployApp, + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -4531,34 +5165,68 @@ describe("app controller", () => { it("logs streams the live deployment for the selected app", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_live" }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_live", + }, ]); const listDeployments = vi.fn().mockResolvedValue({ - app: { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_live" }, + app: { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_live", + }, deployments: [ - { id: "dep_old", status: "stopped", createdAt: "2026-05-01T00:00:00Z", url: null, live: null }, - { id: "dep_live", status: "running", createdAt: "2026-05-02T00:00:00Z", url: "https://example.prisma.app", live: null }, + { + id: "dep_old", + status: "stopped", + createdAt: "2026-05-01T00:00:00Z", + url: null, + live: null, + }, + { + id: "dep_live", + status: "running", + createdAt: "2026-05-02T00:00:00Z", + url: "https://example.prisma.app", + live: null, + }, ], }); - const streamDeploymentLogs = vi.fn().mockImplementation(async (options: { onRecord(record: unknown): void }) => { - options.onRecord({ type: "log", text: "hello from live\n", byteStart: 0, byteEnd: 16 }); - }); - + const streamDeploymentLogs = vi + .fn() + .mockImplementation( + async (options: { onRecord(record: unknown): void }) => { + options.onRecord({ + type: "log", + text: "hello from live\n", + byteStart: 0, + byteEnd: 16, + }); + }, + ); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - listDeployments, - showDeployment: vi.fn(), - streamDeploymentLogs, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + listDeployments, + showDeployment: vi.fn(), + streamDeploymentLogs, + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppLogs } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4576,43 +5244,80 @@ describe("app controller", () => { await runAppLogs(context, "hello-world", undefined); - expect(streamDeploymentLogs).toHaveBeenCalledWith(expect.objectContaining({ - deploymentId: "dep_live", - signal: context.runtime.signal, - })); + expect(streamDeploymentLogs).toHaveBeenCalledWith( + expect.objectContaining({ + deploymentId: "dep_live", + signal: context.runtime.signal, + }), + ); expect(stdout.buffer).toBe("hello from live\n"); }); it("logs streams an explicit deployment for the selected app", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_live" }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_live", + }, ]); const listDeployments = vi.fn().mockResolvedValue({ - app: { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_live" }, + app: { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_live", + }, deployments: [ - { id: "dep_old", status: "stopped", createdAt: "2026-05-01T00:00:00Z", url: null, live: null }, - { id: "dep_live", status: "running", createdAt: "2026-05-02T00:00:00Z", url: "https://example.prisma.app", live: null }, + { + id: "dep_old", + status: "stopped", + createdAt: "2026-05-01T00:00:00Z", + url: null, + live: null, + }, + { + id: "dep_live", + status: "running", + createdAt: "2026-05-02T00:00:00Z", + url: "https://example.prisma.app", + live: null, + }, ], }); - const streamDeploymentLogs = vi.fn().mockImplementation(async (options: { onRecord(record: unknown): void }) => { - options.onRecord({ type: "log", text: "old log\n", byteStart: 0, byteEnd: 8 }); - }); + const streamDeploymentLogs = vi + .fn() + .mockImplementation( + async (options: { onRecord(record: unknown): void }) => { + options.onRecord({ + type: "log", + text: "old log\n", + byteStart: 0, + byteEnd: 8, + }); + }, + ); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - listDeployments, - showDeployment: vi.fn(), - streamDeploymentLogs, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + listDeployments, + showDeployment: vi.fn(), + streamDeploymentLogs, + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppLogs } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4628,21 +5333,39 @@ describe("app controller", () => { await runAppLogs(context, "hello-world", "dep_old"); - expect(streamDeploymentLogs).toHaveBeenCalledWith(expect.objectContaining({ - deploymentId: "dep_old", - })); + expect(streamDeploymentLogs).toHaveBeenCalledWith( + expect.objectContaining({ + deploymentId: "dep_old", + }), + ); expect(stdout.buffer).toBe("old log\n"); }); it("logs rejects an explicit deployment that does not belong to the selected app", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_live" }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_live", + }, ]); const listDeployments = vi.fn().mockResolvedValue({ - app: { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_live" }, + app: { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_live", + }, deployments: [ - { id: "dep_live", status: "running", createdAt: "2026-05-02T00:00:00Z", url: "https://example.prisma.app", live: null }, + { + id: "dep_live", + status: "running", + createdAt: "2026-05-02T00:00:00Z", + url: "https://example.prisma.app", + live: null, + }, ], }); const streamDeploymentLogs = vi.fn(); @@ -4651,16 +5374,20 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - listDeployments, - showDeployment: vi.fn(), - streamDeploymentLogs, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + listDeployments, + showDeployment: vi.fn(), + streamDeploymentLogs, + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppLogs } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4674,7 +5401,9 @@ describe("app controller", () => { }, }); - await expect(runAppLogs(context, "hello-world", "dep_other")).rejects.toMatchObject({ + await expect( + runAppLogs(context, "hello-world", "dep_other"), + ).rejects.toMatchObject({ code: "DEPLOYMENT_NOT_FOUND", domain: "app", }); @@ -4684,30 +5413,64 @@ describe("app controller", () => { it("logs emits newline-delimited JSON events in --json mode", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_live" }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_live", + }, ]); const listDeployments = vi.fn().mockResolvedValue({ - app: { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_live" }, + app: { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_live", + }, deployments: [ - { id: "dep_live", status: "running", createdAt: "2026-05-02T00:00:00Z", url: "https://example.prisma.app", live: null }, + { + id: "dep_live", + status: "running", + createdAt: "2026-05-02T00:00:00Z", + url: "https://example.prisma.app", + live: null, + }, ], }); - const streamDeploymentLogs = vi.fn().mockImplementation(async (options: { onRecord(record: unknown): void }) => { - options.onRecord({ type: "log", text: "json log\n", byteStart: 0, byteEnd: 9 }); - options.onRecord({ type: "terminal", kind: "end", code: "done", message: "done", retryable: false, cursor: null }); - }); + const streamDeploymentLogs = vi + .fn() + .mockImplementation( + async (options: { onRecord(record: unknown): void }) => { + options.onRecord({ + type: "log", + text: "json log\n", + byteStart: 0, + byteEnd: 9, + }); + options.onRecord({ + type: "terminal", + kind: "end", + code: "done", + message: "done", + retryable: false, + cursor: null, + }); + }, + ); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - listApps, - listDeployments, - showDeployment: vi.fn(), - streamDeploymentLogs, - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + listApps, + listDeployments, + showDeployment: vi.fn(), + streamDeploymentLogs, + }), + ), })); const { createTempCwd, executeCli } = await import("./helpers"); @@ -4726,7 +5489,10 @@ describe("app controller", () => { }, }); - const events = result.stdout.trim().split("\n").map((line) => JSON.parse(line)); + const events = result.stdout + .trim() + .split("\n") + .map((line) => JSON.parse(line)); expect(result.exitCode).toBe(0); expect(result.stderr).toBe(""); @@ -4751,7 +5517,12 @@ describe("app controller", () => { it("remove deletes the selected app when --yes is passed", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_2" }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_2", + }, ]); const removeApp = vi.fn().mockResolvedValue({ id: "app_1", @@ -4762,19 +5533,23 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - removeApp, - promoteDeployment: vi.fn(), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + removeApp, + promoteDeployment: vi.fn(), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRemove } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4795,11 +5570,17 @@ describe("app controller", () => { id: "app_1", name: "hello-world", }); - await context.stateStore.setKnownLiveDeployment("proj_123", "app_1", "dep_2"); + await context.stateStore.setKnownLiveDeployment( + "proj_123", + "app_1", + "dep_2", + ); const result = await runAppRemove(context, "hello-world"); - expect(removeApp).toHaveBeenCalledWith("app_1", { signal: context.runtime.signal }); + expect(removeApp).toHaveBeenCalledWith("app_1", { + signal: context.runtime.signal, + }); expect(result.result).toEqual({ projectId: "proj_123", verboseContext: expectedAppVerboseContext(), @@ -4809,14 +5590,23 @@ describe("app controller", () => { }, removed: true, }); - await expect(context.stateStore.readSelectedApp("proj_123")).resolves.toBeNull(); - await expect(context.stateStore.readKnownLiveDeployment("proj_123", "app_1")).resolves.toBeNull(); + await expect( + context.stateStore.readSelectedApp("proj_123"), + ).resolves.toBeNull(); + await expect( + context.stateStore.readKnownLiveDeployment("proj_123", "app_1"), + ).resolves.toBeNull(); }); it("remove prompts for confirmation in interactive mode", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_2" }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_2", + }, ]); const removeApp = vi.fn().mockResolvedValue({ id: "app_1", @@ -4828,26 +5618,32 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/shell/prompt", async () => { - const actual = await vi.importActual("../src/shell/prompt"); + const actual = await vi.importActual< + typeof import("../src/shell/prompt") + >("../src/shell/prompt"); return { ...actual, textPrompt, }; }); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - removeApp, - promoteDeployment: vi.fn(), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + removeApp, + promoteDeployment: vi.fn(), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRemove } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4870,13 +5666,20 @@ describe("app controller", () => { placeholder: "hello-world", }), ); - expect(removeApp).toHaveBeenCalledWith("app_1", { signal: context.runtime.signal }); + expect(removeApp).toHaveBeenCalledWith("app_1", { + signal: context.runtime.signal, + }); }); it("remove returns CONFIRMATION_REQUIRED in non-interactive mode without --yes", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_2" }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_2", + }, ]); const removeApp = vi.fn(); @@ -4884,19 +5687,23 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - removeApp, - promoteDeployment: vi.fn(), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + removeApp, + promoteDeployment: vi.fn(), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRemove } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4922,27 +5729,38 @@ describe("app controller", () => { it("remove returns REMOVE_FAILED when remote deletion fails", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_2" }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_2", + }, ]); - const removeApp = vi.fn().mockRejectedValue(new Error("Resource Not Found")); + const removeApp = vi + .fn() + .mockRejectedValue(new Error("Resource Not Found")); vi.doMock("../src/lib/auth/guard", () => ({ requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - removeApp, - promoteDeployment: vi.fn(), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + removeApp, + promoteDeployment: vi.fn(), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRemove } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -4969,7 +5787,12 @@ describe("app controller", () => { it("remove returns a warning when local cleanup fails after remote deletion", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: "dep_2" }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: "dep_2", + }, ]); const removeApp = vi.fn().mockResolvedValue({ id: "app_1", @@ -4980,19 +5803,23 @@ describe("app controller", () => { requireComputeAuth, })); vi.doMock("../src/lib/app/preview-provider", () => ({ - createPreviewAppProvider: vi.fn(() => withBranchDatabaseProviderDefaults({ - resolveBranch: createResolveBranch(), - createProject: vi.fn(), - listApps, - removeApp, - promoteDeployment: vi.fn(), - deployApp: vi.fn(), - listDeployments: vi.fn(), - showDeployment: vi.fn(), - })), + createPreviewAppProvider: vi.fn(() => + withBranchDatabaseProviderDefaults({ + resolveBranch: createResolveBranch(), + createProject: vi.fn(), + listApps, + removeApp, + promoteDeployment: vi.fn(), + deployApp: vi.fn(), + listDeployments: vi.fn(), + showDeployment: vi.fn(), + }), + ), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRemove } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeLocalPin(cwd, { workspaceId: "ws_123", projectId: "proj_123" }); @@ -5009,8 +5836,12 @@ describe("app controller", () => { }, }); - vi.spyOn(context.stateStore, "clearSelectedApp").mockRejectedValue(new Error("disk full")); - vi.spyOn(context.stateStore, "clearKnownLiveDeployment").mockResolvedValue(await context.stateStore.read()); + vi.spyOn(context.stateStore, "clearSelectedApp").mockRejectedValue( + new Error("disk full"), + ); + vi.spyOn(context.stateStore, "clearKnownLiveDeployment").mockResolvedValue( + await context.stateStore.read(), + ); const result = await runAppRemove(context, "hello-world"); diff --git a/packages/cli/tests/app-env-presenter.test.ts b/packages/cli/tests/app-env-presenter.test.ts index d012bdc..f482fe4 100644 --- a/packages/cli/tests/app-env-presenter.test.ts +++ b/packages/cli/tests/app-env-presenter.test.ts @@ -30,15 +30,19 @@ describe("app env presenters", () => { ], }; - const human = stripAnsi(renderEnvList( - context, - getCommandDescriptor("project.env.list"), - result, - ).join("\n")); + const human = stripAnsi( + renderEnvList( + context, + getCommandDescriptor("project.env.list"), + result, + ).join("\n"), + ); const json = serializeEnvList(result); expect(human).toContain("target:"); - expect(human).toContain("branch:feature/not-created -> preview (not created yet)"); + expect(human).toContain( + "branch:feature/not-created -> preview (not created yet)", + ); expect(json).toMatchObject({ projectId: "proj_123", scope: { kind: "role", role: "preview" }, diff --git a/packages/cli/tests/app-env-vars.test.ts b/packages/cli/tests/app-env-vars.test.ts index ebfe964..c21e29e 100644 --- a/packages/cli/tests/app-env-vars.test.ts +++ b/packages/cli/tests/app-env-vars.test.ts @@ -65,11 +65,15 @@ function createProjectClient() { } function createResolveBranch(role: "preview" | "production" = "preview") { - return vi.fn().mockImplementation((_projectId: string, options: { branchName: string }) => Promise.resolve({ - id: `branch_${options.branchName.replace(/[^a-z0-9]+/gi, "_")}`, - name: options.branchName, - role, - })); + return vi + .fn() + .mockImplementation((_projectId: string, options: { branchName: string }) => + Promise.resolve({ + id: `branch_${options.branchName.replace(/[^a-z0-9]+/gi, "_")}`, + name: options.branchName, + role, + }), + ); } describe("app env vars", () => { @@ -81,7 +85,7 @@ describe("app env vars", () => { [ "# local settings", "API_URL=https://api.example", - "QUOTED=\"hello world\"", + 'QUOTED="hello world"', "export FEATURE_FLAG=enabled", "LITERAL=${API_URL}/v1", ].join("\n"), @@ -102,9 +106,9 @@ describe("app env vars", () => { expect( parseEnvFileContents( [ - "CERT=\"-----BEGIN CERT-----", + 'CERT="-----BEGIN CERT-----', "API_URL=https://inside.example", - "-----END CERT-----\"", + '-----END CERT-----"', "API_URL=https://api.example", ].join("\n"), ".env", @@ -113,7 +117,8 @@ describe("app env vars", () => { ).toEqual([ { key: "CERT", - value: "-----BEGIN CERT-----\nAPI_URL=https://inside.example\n-----END CERT-----", + value: + "-----BEGIN CERT-----\nAPI_URL=https://inside.example\n-----END CERT-----", }, { key: "API_URL", value: "https://api.example" }, ]); @@ -123,26 +128,36 @@ describe("app env vars", () => { const { parseEnvFileContents } = await import("../src/lib/app/env-file"); expect(() => - parseEnvFileContents("API_URL=https://first\nAPI_URL=https://second\n", ".env", "add"), - ).toThrowError(expect.objectContaining({ - code: "USAGE_ERROR", - summary: 'Duplicate environment variable "API_URL" in ".env"', - })); + parseEnvFileContents( + "API_URL=https://first\nAPI_URL=https://second\n", + ".env", + "add", + ), + ).toThrowError( + expect.objectContaining({ + code: "USAGE_ERROR", + summary: 'Duplicate environment variable "API_URL" in ".env"', + }), + ); expect(() => parseEnvFileContents("lowercase-key=secret\n", ".env", "add"), - ).toThrowError(expect.objectContaining({ - code: "USAGE_ERROR", - summary: 'Invalid environment variable "lowercase-key" in ".env"', - })); + ).toThrowError( + expect.objectContaining({ + code: "USAGE_ERROR", + summary: 'Invalid environment variable "lowercase-key" in ".env"', + }), + ); const longKey = `A${"B".repeat(256)}`; expect(() => parseEnvFileContents(`${longKey}=secret\n`, ".env", "add"), - ).toThrowError(expect.objectContaining({ - code: "USAGE_ERROR", - why: expect.stringContaining("exceeds the 256-character limit"), - })); + ).toThrowError( + expect.objectContaining({ + code: "USAGE_ERROR", + why: expect.stringContaining("exceeds the 256-character limit"), + }), + ); let emptyValueError: unknown; try { @@ -163,10 +178,7 @@ describe("app env vars", () => { expect( parseEnvAssignments( - [ - "DATABASE_URL=postgresql://example", - "TOKEN=value=with=equals", - ], + ["DATABASE_URL=postgresql://example", "TOKEN=value=with=equals"], { commandName: "deploy" }, ), ).toEqual({ @@ -181,14 +193,13 @@ describe("app env vars", () => { const cwd = await createTempCwd(); await writeFile( path.join(cwd, ".env"), - [ - "DATABASE_URL=postgresql://example", - "FEATURE_FLAG=enabled", - ].join("\n"), + ["DATABASE_URL=postgresql://example", "FEATURE_FLAG=enabled"].join("\n"), ); await expect( - parseEnvInputs(cwd, [".env", "INLINE_FLAG=enabled"], { commandName: "deploy" }), + parseEnvInputs(cwd, [".env", "INLINE_FLAG=enabled"], { + commandName: "deploy", + }), ).resolves.toEqual({ DATABASE_URL: "postgresql://example", FEATURE_FLAG: "enabled", @@ -199,26 +210,34 @@ describe("app env vars", () => { it("rejects invalid env assignments without leaking values", async () => { const { parseEnvAssignments } = await import("../src/lib/app/env-vars"); - expect(() => parseEnvAssignments(["DATABASE_URL"], { commandName: "deploy" })).toThrowError( + expect(() => + parseEnvAssignments(["DATABASE_URL"], { commandName: "deploy" }), + ).toThrowError( expect.objectContaining({ code: "USAGE_ERROR", summary: "Environment variable assignment must use NAME=VALUE", }), ); - expect(() => parseEnvAssignments(["=secret"], { commandName: "deploy" })).toThrowError( + expect(() => + parseEnvAssignments(["=secret"], { commandName: "deploy" }), + ).toThrowError( expect.objectContaining({ code: "USAGE_ERROR", summary: "Environment variable name is required", }), ); - expect(() => parseEnvAssignments(["lowercase-key=secret"], { commandName: "deploy" })).toThrowError( + expect(() => + parseEnvAssignments(["lowercase-key=secret"], { commandName: "deploy" }), + ).toThrowError( expect.objectContaining({ code: "USAGE_ERROR", summary: 'Invalid environment variable "lowercase-key"', why: expect.stringContaining("must match the POSIX env-var shape"), }), ); - expect(() => parseEnvAssignments(["EMPTY="], { commandName: "deploy" })).toThrowError( + expect(() => + parseEnvAssignments(["EMPTY="], { commandName: "deploy" }), + ).toThrowError( expect.objectContaining({ code: "USAGE_ERROR", summary: 'Environment variable "EMPTY" has an empty value', @@ -227,16 +246,14 @@ describe("app env vars", () => { try { parseEnvAssignments( - [ - "DATABASE_URL=postgresql://first", - "DATABASE_URL=postgresql://second", - ], + ["DATABASE_URL=postgresql://first", "DATABASE_URL=postgresql://second"], { commandName: "deploy" }, ); } catch (error) { expect(error).toMatchObject({ code: "USAGE_ERROR", - summary: 'Environment variable "DATABASE_URL" was provided more than once', + summary: + 'Environment variable "DATABASE_URL" was provided more than once', }); expect(JSON.stringify(error)).not.toContain("postgresql://first"); expect(JSON.stringify(error)).not.toContain("postgresql://second"); @@ -263,10 +280,15 @@ describe("app env vars", () => { requireComputeAuth, })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runEnvList } = await import("../src/controllers/app-env"); const cwd = await createTempCwd(); - await writeFile(path.join(cwd, "package.json"), `${JSON.stringify({ name: "acme-dashboard" }, null, 2)}\n`); + await writeFile( + path.join(cwd, "package.json"), + `${JSON.stringify({ name: "acme-dashboard" }, null, 2)}\n`, + ); const stateDir = path.join(cwd, ".state"); const { context } = await createTestCommandContext({ cwd, @@ -330,7 +352,9 @@ describe("app env vars", () => { requireComputeAuth, })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runEnvList } = await import("../src/controllers/app-env"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -378,7 +402,9 @@ describe("app env vars", () => { }); vi.doMock("../src/controllers/app-env", async () => { - const actual = await vi.importActual("../src/controllers/app-env"); + const actual = await vi.importActual< + typeof import("../src/controllers/app-env") + >("../src/controllers/app-env"); return { ...actual, runEnvAdd, @@ -426,22 +452,24 @@ describe("app env vars", () => { ], }, }); - expect(runEnvAdd).toHaveBeenCalledWith( - expect.anything(), - undefined, - { - roleName: "preview", - branchName: undefined, - projectRef: "proj_123", - filePath: ".env", - }, - ); + expect(runEnvAdd).toHaveBeenCalledWith(expect.anything(), undefined, { + roleName: "preview", + branchName: undefined, + projectRef: "proj_123", + filePath: ".env", + }); }); it("passes env vars to provider deploy without surfacing values", async () => { const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient()); const listApps = vi.fn().mockResolvedValue([ - { id: "app_1", name: "hello-world", region: "eu-central-1", liveDeploymentId: null, liveUrl: null }, + { + id: "app_1", + name: "hello-world", + region: "eu-central-1", + liveDeploymentId: null, + liveUrl: null, + }, ]); const deployApp = vi.fn().mockResolvedValue({ projectId: "proj_123", @@ -477,7 +505,9 @@ describe("app env vars", () => { })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppDeploy } = await import("../src/controllers/app"); const cwd = await createTempCwd(); await writeFile(path.join(cwd, ".env"), "FEATURE_FLAG=enabled\n"); @@ -491,15 +521,15 @@ describe("app env vars", () => { }, }); - const result = await runAppDeploy( - context, - "hello-world", - { - projectRef: "proj_123", - framework: "hono", - envAssignments: ["DATABASE_URL=postgresql://example", ".env", "INLINE_FLAG=enabled"], - }, - ); + const result = await runAppDeploy(context, "hello-world", { + projectRef: "proj_123", + framework: "hono", + envAssignments: [ + "DATABASE_URL=postgresql://example", + ".env", + "INLINE_FLAG=enabled", + ], + }); expect(deployApp).toHaveBeenCalledWith( expect.objectContaining({ @@ -574,7 +604,9 @@ describe("app env vars", () => { }); vi.doMock("../src/controllers/app", async () => { - const actual = await vi.importActual("../src/controllers/app"); + const actual = await vi.importActual< + typeof import("../src/controllers/app") + >("../src/controllers/app"); return { ...actual, runAppDeploy, diff --git a/packages/cli/tests/app-env.test.ts b/packages/cli/tests/app-env.test.ts index 0e5e073..337f78f 100644 --- a/packages/cli/tests/app-env.test.ts +++ b/packages/cli/tests/app-env.test.ts @@ -96,7 +96,11 @@ async function writeLocalPin(cwd: string, projectId = "proj_123") { async function writeGitHead(cwd: string, branchName: string) { await mkdir(path.join(cwd, ".git"), { recursive: true }); - await writeFile(path.join(cwd, ".git", "HEAD"), `ref: refs/heads/${branchName}\n`, "utf8"); + await writeFile( + path.join(cwd, ".git", "HEAD"), + `ref: refs/heads/${branchName}\n`, + "utf8", + ); } async function loadControllers(client: MockClient, projectId: string) { @@ -127,14 +131,16 @@ async function loadControllers(client: MockClient, projectId: string) { return { controllers, createTempCwd, createTestCommandContext }; } -function makeVariableRow(overrides: Partial<{ - id: string; - key: string; - branchId: string | null; - class: "production" | "preview"; - isManagedBySystem: boolean; - updatedAt: string; -}> = {}) { +function makeVariableRow( + overrides: Partial<{ + id: string; + key: string; + branchId: string | null; + class: "production" | "preview"; + isManagedBySystem: boolean; + updatedAt: string; + }> = {}, +) { return { id: "envvar_v1", type: "environment-variable", @@ -151,12 +157,14 @@ function makeVariableRow(overrides: Partial<{ }; } -function makeBranchRow(overrides: Partial<{ - id: string; - gitName: string; - role: "production" | "preview"; - isDefault: boolean; -}> = {}) { +function makeBranchRow( + overrides: Partial<{ + id: string; + gitName: string; + role: "production" | "preview"; + isDefault: boolean; + }> = {}, +) { return { id: "br_feature", gitName: "feature/foo", @@ -260,15 +268,25 @@ describe("env add", () => { data: { data: [], pagination: { hasMore: false, nextCursor: null } }, response: { status: 200 }, }); - client.POST - .mockResolvedValueOnce({ - data: { data: makeVariableRow({ id: "envvar_api", key: "API_URL", class: "preview" }) }, - response: { status: 201 }, - }) - .mockResolvedValueOnce({ - data: { data: makeVariableRow({ id: "envvar_stripe", key: "STRIPE_KEY", class: "preview" }) }, - response: { status: 201 }, - }); + client.POST.mockResolvedValueOnce({ + data: { + data: makeVariableRow({ + id: "envvar_api", + key: "API_URL", + class: "preview", + }), + }, + response: { status: 201 }, + }).mockResolvedValueOnce({ + data: { + data: makeVariableRow({ + id: "envvar_stripe", + key: "STRIPE_KEY", + class: "preview", + }), + }, + response: { status: 201 }, + }); const { controllers, createTempCwd, createTestCommandContext } = await loadControllers(client, "proj_123"); @@ -281,11 +299,10 @@ describe("env add", () => { ); const { context } = await createTestCommandContext({ cwd }); - const result = await controllers.runEnvAdd( - context, - undefined, - { roleName: "preview", filePath: ".env" }, - ); + const result = await controllers.runEnvAdd(context, undefined, { + roleName: "preview", + filePath: ".env", + }); expect(client.POST).toHaveBeenNthCalledWith( 1, @@ -359,7 +376,7 @@ describe("env add", () => { keys: ["STRIPE_KEY"], }, nextSteps: [ - "# existing keys: \"STRIPE_KEY\"", + '# existing keys: "STRIPE_KEY"', "prisma-cli project env update --file .env.existing --role preview", "# new keys only", "prisma-cli project env add --file .env.new --role preview", @@ -399,7 +416,10 @@ describe("env add", () => { const client = createMockClient(); client.envGET .mockResolvedValueOnce({ - data: { data: [makeBranchRow()], pagination: { hasMore: false, nextCursor: null } }, + data: { + data: [makeBranchRow()], + pagination: { hasMore: false, nextCursor: null }, + }, response: { status: 200 }, }) .mockResolvedValueOnce({ @@ -458,7 +478,10 @@ describe("env add", () => { const client = createMockClient(); client.envGET .mockResolvedValueOnce({ - data: { data: [makeBranchRow()], pagination: { hasMore: false, nextCursor: null } }, + data: { + data: [makeBranchRow()], + pagination: { hasMore: false, nextCursor: null }, + }, response: { status: 200 }, }) .mockResolvedValueOnce({ @@ -484,7 +507,11 @@ describe("env add", () => { await loadControllers(client, "proj_123"); const cwd = await createTempCwd(); await writeLocalPin(cwd); - await writeFile(path.join(cwd, ".env.local"), "DATABASE_URL=postgresql://branch\n", "utf8"); + await writeFile( + path.join(cwd, ".env.local"), + "DATABASE_URL=postgresql://branch\n", + "utf8", + ); const { context } = await createTestCommandContext({ cwd }); const result = await controllers.runEnvAdd(context, undefined, { @@ -523,19 +550,23 @@ describe("env add", () => { data: { data: [], pagination: { hasMore: false, nextCursor: null } }, response: { status: 200 }, }); - client.POST - .mockResolvedValueOnce({ - data: { data: makeVariableRow({ id: "envvar_api", key: "API_URL", class: "preview" }) }, - response: { status: 201 }, - }) - .mockResolvedValueOnce({ + client.POST.mockResolvedValueOnce({ + data: { + data: makeVariableRow({ + id: "envvar_api", + key: "API_URL", + class: "preview", + }), + }, + response: { status: 201 }, + }).mockResolvedValueOnce({ + error: { error: { - error: { - message: "Environment variable service is unavailable.", - }, + message: "Environment variable service is unavailable.", }, - response: { status: 503 }, - }); + }, + response: { status: 503 }, + }); const { controllers, createTempCwd, createTestCommandContext } = await loadControllers(client, "proj_123"); @@ -577,7 +608,9 @@ describe("env add", () => { }) .mockResolvedValueOnce({ data: { - data: [makeBranchRow({ id: "br_main", gitName: "main", isDefault: true })], + data: [ + makeBranchRow({ id: "br_main", gitName: "main", isDefault: true }), + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, @@ -590,21 +623,19 @@ describe("env add", () => { data: { data: [], pagination: { hasMore: false, nextCursor: null } }, response: { status: 200 }, }); - client.POST - .mockResolvedValueOnce({ - data: { data: makeBranchRow({ id: "br_new", gitName: "feature/new" }) }, - response: { status: 201 }, - }) - .mockResolvedValueOnce({ - data: { - data: makeVariableRow({ - key: "DATABASE_URL", - branchId: "br_new", - class: "preview", - }), - }, - response: { status: 201 }, - }); + client.POST.mockResolvedValueOnce({ + data: { data: makeBranchRow({ id: "br_new", gitName: "feature/new" }) }, + response: { status: 201 }, + }).mockResolvedValueOnce({ + data: { + data: makeVariableRow({ + key: "DATABASE_URL", + branchId: "br_new", + class: "preview", + }), + }, + response: { status: 201 }, + }); const { controllers, createTempCwd, createTestCommandContext } = await loadControllers(client, "proj_123"); @@ -635,7 +666,9 @@ describe("env add", () => { }) .mockResolvedValueOnce({ data: { - data: [makeBranchRow({ id: "br_main", gitName: "main", isDefault: true })], + data: [ + makeBranchRow({ id: "br_main", gitName: "main", isDefault: true }), + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, @@ -798,7 +831,9 @@ describe("env update", () => { response: { status: 200 }, }); client.PATCH.mockResolvedValueOnce({ - data: { data: makeVariableRow({ updatedAt: "2026-05-08T11:00:00.000Z" }) }, + data: { + data: makeVariableRow({ updatedAt: "2026-05-08T11:00:00.000Z" }), + }, response: { status: 200 }, }); @@ -857,7 +892,10 @@ describe("env update", () => { const client = createMockClient(); client.envGET .mockResolvedValueOnce({ - data: { data: [makeBranchRow()], pagination: { hasMore: false, nextCursor: null } }, + data: { + data: [makeBranchRow()], + pagination: { hasMore: false, nextCursor: null }, + }, response: { status: 200 }, }) .mockResolvedValueOnce({ @@ -892,11 +930,9 @@ describe("env update", () => { await writeLocalPin(cwd); const { context } = await createTestCommandContext({ cwd }); - await controllers.runEnvUpdate( - context, - "DATABASE_URL=postgresql://new", - { branchName: "feature/foo" }, - ); + await controllers.runEnvUpdate(context, "DATABASE_URL=postgresql://new", { + branchName: "feature/foo", + }); expect(client.PATCH).toHaveBeenCalledWith( "/v1/environment-variables/{envVarId}", @@ -912,27 +948,49 @@ describe("env update", () => { client.envGET .mockResolvedValueOnce({ data: { - data: [makeVariableRow({ id: "envvar_api", key: "API_URL", class: "preview" })], + data: [ + makeVariableRow({ + id: "envvar_api", + key: "API_URL", + class: "preview", + }), + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, }) .mockResolvedValueOnce({ data: { - data: [makeVariableRow({ id: "envvar_stripe", key: "STRIPE_KEY", class: "preview" })], + data: [ + makeVariableRow({ + id: "envvar_stripe", + key: "STRIPE_KEY", + class: "preview", + }), + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, }); - client.PATCH - .mockResolvedValueOnce({ - data: { data: makeVariableRow({ id: "envvar_api", key: "API_URL", class: "preview" }) }, - response: { status: 200 }, - }) - .mockResolvedValueOnce({ - data: { data: makeVariableRow({ id: "envvar_stripe", key: "STRIPE_KEY", class: "preview" }) }, - response: { status: 200 }, - }); + client.PATCH.mockResolvedValueOnce({ + data: { + data: makeVariableRow({ + id: "envvar_api", + key: "API_URL", + class: "preview", + }), + }, + response: { status: 200 }, + }).mockResolvedValueOnce({ + data: { + data: makeVariableRow({ + id: "envvar_stripe", + key: "STRIPE_KEY", + class: "preview", + }), + }, + response: { status: 200 }, + }); const { controllers, createTempCwd, createTestCommandContext } = await loadControllers(client, "proj_123"); @@ -981,7 +1039,13 @@ describe("env update", () => { client.envGET .mockResolvedValueOnce({ data: { - data: [makeVariableRow({ id: "envvar_api", key: "API_URL", class: "preview" })], + data: [ + makeVariableRow({ + id: "envvar_api", + key: "API_URL", + class: "preview", + }), + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, @@ -1013,7 +1077,7 @@ describe("env update", () => { keys: ["STRIPE_KEY"], }, nextSteps: [ - "# missing keys: \"STRIPE_KEY\"", + '# missing keys: "STRIPE_KEY"', "prisma-cli project env add --file .env.new --role preview", "# existing keys only", "prisma-cli project env update --file .env.existing --role preview", @@ -1092,7 +1156,13 @@ describe("env list", () => { client.envGET .mockResolvedValueOnce({ data: { - data: [makeBranchRow({ id: "br_feature", gitName: "feature/foo", role: "preview" })], + data: [ + makeBranchRow({ + id: "br_feature", + gitName: "feature/foo", + role: "preview", + }), + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, @@ -1160,12 +1230,18 @@ describe("env list", () => { branchExists: true, envMap: "preview", }); - expect(result.result.variables.map((variable) => ({ - key: variable.key, - id: variable.id, - source: variable.source, - }))).toEqual([ - { key: "DATABASE_URL", id: "envvar_branch", source: "branch:feature/foo" }, + expect( + result.result.variables.map((variable) => ({ + key: variable.key, + id: variable.id, + source: variable.source, + })), + ).toEqual([ + { + key: "DATABASE_URL", + id: "envvar_branch", + source: "branch:feature/foo", + }, ]); }); @@ -1174,19 +1250,27 @@ describe("env list", () => { client.envGET .mockResolvedValueOnce({ data: { - data: [makeBranchRow({ - id: "br_main", - gitName: "main", - role: "production", - isDefault: true, - })], + data: [ + makeBranchRow({ + id: "br_main", + gitName: "main", + role: "production", + isDefault: true, + }), + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, }) .mockResolvedValueOnce({ data: { - data: [makeVariableRow({ id: "envvar_prod", key: "STRIPE_KEY", class: "production" })], + data: [ + makeVariableRow({ + id: "envvar_prod", + key: "STRIPE_KEY", + class: "production", + }), + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, @@ -1221,12 +1305,12 @@ describe("env list", () => { branchExists: true, envMap: "production", }); - expect(result.result.variables.map((variable) => ({ - key: variable.key, - source: variable.source, - }))).toEqual([ - { key: "STRIPE_KEY", source: "production" }, - ]); + expect( + result.result.variables.map((variable) => ({ + key: variable.key, + source: variable.source, + })), + ).toEqual([{ key: "STRIPE_KEY", source: "production" }]); }); it("shows preview template metadata when the active Git branch has no Platform branch yet", async () => { @@ -1273,12 +1357,12 @@ describe("env list", () => { branchExists: false, envMap: "preview", }); - expect(result.result.variables.map((variable) => ({ - key: variable.key, - source: variable.source, - }))).toEqual([ - { key: "API_URL", source: "preview" }, - ]); + expect( + result.result.variables.map((variable) => ({ + key: variable.key, + source: variable.source, + })), + ).toEqual([{ key: "API_URL", source: "preview" }]); }); it("shows a production and preview overview when no local Git branch exists", async () => { @@ -1286,8 +1370,16 @@ describe("env list", () => { client.envGET.mockResolvedValueOnce({ data: { data: [ - makeVariableRow({ id: "envvar_preview", key: "API_URL", class: "preview" }), - makeVariableRow({ id: "envvar_prod", key: "STRIPE_KEY", class: "production" }), + makeVariableRow({ + id: "envvar_preview", + key: "API_URL", + class: "preview", + }), + makeVariableRow({ + id: "envvar_prod", + key: "STRIPE_KEY", + class: "production", + }), makeVariableRow({ id: "envvar_branch", key: "DATABASE_URL", @@ -1323,10 +1415,12 @@ describe("env list", () => { source: "overview", envMap: "overview", }); - expect(result.result.variables.map((variable) => ({ - key: variable.key, - source: variable.source, - }))).toEqual([ + expect( + result.result.variables.map((variable) => ({ + key: variable.key, + source: variable.source, + })), + ).toEqual([ { key: "STRIPE_KEY", source: "production" }, { key: "API_URL", source: "preview" }, ]); @@ -1336,7 +1430,10 @@ describe("env list", () => { const client = createMockClient(); client.envGET .mockResolvedValueOnce({ - data: { data: [makeBranchRow()], pagination: { hasMore: false, nextCursor: null } }, + data: { + data: [makeBranchRow()], + pagination: { hasMore: false, nextCursor: null }, + }, response: { status: 200 }, }) .mockResolvedValueOnce({ @@ -1389,13 +1486,19 @@ describe("env list", () => { branchExists: true, envMap: "preview", }); - expect(result.result.variables.map((variable) => ({ - key: variable.key, - id: variable.id, - source: variable.source, - }))).toEqual([ + expect( + result.result.variables.map((variable) => ({ + key: variable.key, + id: variable.id, + source: variable.source, + })), + ).toEqual([ { key: "API_URL", id: "envvar_api", source: "preview" }, - { key: "DATABASE_URL", id: "envvar_branch", source: "branch:feature/foo" }, + { + key: "DATABASE_URL", + id: "envvar_branch", + source: "branch:feature/foo", + }, ]); }); }); @@ -1466,7 +1569,10 @@ describe("env remove", () => { const client = createMockClient(); client.envGET .mockResolvedValueOnce({ - data: { data: [makeBranchRow()], pagination: { hasMore: false, nextCursor: null } }, + data: { + data: [makeBranchRow()], + pagination: { hasMore: false, nextCursor: null }, + }, response: { status: 200 }, }) .mockResolvedValueOnce({ diff --git a/packages/cli/tests/app-local-dev.test.ts b/packages/cli/tests/app-local-dev.test.ts index a2bda4c..8fc9238 100644 --- a/packages/cli/tests/app-local-dev.test.ts +++ b/packages/cli/tests/app-local-dev.test.ts @@ -20,16 +20,18 @@ describe("app local dev commands", () => { }); vi.doMock("../src/lib/app/preview-build", async () => { - const actual = await vi.importActual( - "../src/lib/app/preview-build", - ); + const actual = await vi.importActual< + typeof import("../src/lib/app/preview-build") + >("../src/lib/app/preview-build"); return { ...actual, executePreviewBuild, }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppBuild } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -63,16 +65,18 @@ describe("app local dev commands", () => { }); vi.doMock("../src/lib/app/preview-build", async () => { - const actual = await vi.importActual( - "../src/lib/app/preview-build", - ); + const actual = await vi.importActual< + typeof import("../src/lib/app/preview-build") + >("../src/lib/app/preview-build"); return { ...actual, executePreviewBuild, }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppBuild } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -97,21 +101,27 @@ describe("app local dev commands", () => { }); it("build returns USAGE_ERROR when framework detection is ambiguous", async () => { - const executePreviewBuild = vi.fn().mockRejectedValue( - new Error("Entrypoint is required. Pass --entry or define package.json main or module."), - ); + const executePreviewBuild = vi + .fn() + .mockRejectedValue( + new Error( + "Entrypoint is required. Pass --entry or define package.json main or module.", + ), + ); vi.doMock("../src/lib/app/preview-build", async () => { - const actual = await vi.importActual( - "../src/lib/app/preview-build", - ); + const actual = await vi.importActual< + typeof import("../src/lib/app/preview-build") + >("../src/lib/app/preview-build"); return { ...actual, executePreviewBuild, }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppBuild } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -120,15 +130,20 @@ describe("app local dev commands", () => { stateDir, }); - await expect(runAppBuild(context, undefined, "auto")).rejects.toMatchObject({ - code: "USAGE_ERROR", - domain: "app", - summary: "App build requires an explicit framework when detection is ambiguous", - }); + await expect(runAppBuild(context, undefined, "auto")).rejects.toMatchObject( + { + code: "USAGE_ERROR", + domain: "app", + summary: + "App build requires an explicit framework when detection is ambiguous", + }, + ); }); it("run returns USAGE_ERROR for --json", async () => { - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRun } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -140,7 +155,9 @@ describe("app local dev commands", () => { }, }); - await expect(runAppRun(context, undefined, "auto", undefined)).rejects.toMatchObject({ + await expect( + runAppRun(context, undefined, "auto", undefined), + ).rejects.toMatchObject({ code: "USAGE_ERROR", domain: "app", summary: "App run does not support --json", @@ -151,16 +168,18 @@ describe("app local dev commands", () => { const resolveLocalBuildType = vi.fn().mockResolvedValue(null); vi.doMock("../src/lib/app/local-dev", async () => { - const actual = await vi.importActual( - "../src/lib/app/local-dev", - ); + const actual = await vi.importActual< + typeof import("../src/lib/app/local-dev") + >("../src/lib/app/local-dev"); return { ...actual, resolveLocalBuildType, }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRun } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -169,15 +188,20 @@ describe("app local dev commands", () => { stateDir, }); - await expect(runAppRun(context, undefined, "auto", undefined)).rejects.toMatchObject({ + await expect( + runAppRun(context, undefined, "auto", undefined), + ).rejects.toMatchObject({ code: "USAGE_ERROR", domain: "app", - summary: "App run requires an explicit framework when detection is ambiguous", + summary: + "App run requires an explicit framework when detection is ambiguous", }); }); it("run rejects --entry together with --build-type nextjs", async () => { - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRun } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -186,7 +210,9 @@ describe("app local dev commands", () => { stateDir, }); - await expect(runAppRun(context, "server.ts", "nextjs", undefined)).rejects.toMatchObject({ + await expect( + runAppRun(context, "server.ts", "nextjs", undefined), + ).rejects.toMatchObject({ code: "USAGE_ERROR", domain: "app", summary: "App run does not accept --entry with --build-type nextjs", @@ -204,16 +230,18 @@ describe("app local dev commands", () => { }); vi.doMock("../src/lib/app/local-dev", async () => { - const actual = await vi.importActual( - "../src/lib/app/local-dev", - ); + const actual = await vi.importActual< + typeof import("../src/lib/app/local-dev") + >("../src/lib/app/local-dev"); return { ...actual, runLocalApp, }; }); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAppRun } = await import("../src/controllers/app"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); diff --git a/packages/cli/tests/app-presenter.test.ts b/packages/cli/tests/app-presenter.test.ts index ab426da..2109e93 100644 --- a/packages/cli/tests/app-presenter.test.ts +++ b/packages/cli/tests/app-presenter.test.ts @@ -1,7 +1,13 @@ import { describe, expect, it } from "vitest"; import { getCommandDescriptor } from "../src/shell/command-meta"; -import { renderAppDeploy, renderAppDomainAdd, renderAppDomainRetry, renderAppDomainShow, serializeAppDeploy } from "../src/presenters/app"; +import { + renderAppDeploy, + renderAppDomainAdd, + renderAppDomainRetry, + renderAppDomainShow, + serializeAppDeploy, +} from "../src/presenters/app"; import type { AppDeployResult, AppDomainAddResult, @@ -11,7 +17,9 @@ import type { } from "../src/types/app"; import { createTestCommandContext } from "./helpers"; -function createDomain(overrides: Partial = {}): AppDomainSummary { +function createDomain( + overrides: Partial = {}, +): AppDomainSummary { return { id: "dom_123", type: "custom-domain", @@ -176,8 +184,12 @@ describe("app domain presenters", () => { ).join("\n"); expect(lines).toContain("dns record"); - expect(lines).toContain("CNAME shop.acme.com -> switchboard.fra.prisma.build ttl 300"); - expect(lines).toContain("Add CNAME shop.acme.com -> switchboard.fra.prisma.build, then run prisma-cli app domain retry shop.acme.com."); + expect(lines).toContain( + "CNAME shop.acme.com -> switchboard.fra.prisma.build ttl 300", + ); + expect(lines).toContain( + "Add CNAME shop.acme.com -> switchboard.fra.prisma.build, then run prisma-cli app domain retry shop.acme.com.", + ); }); }); @@ -217,7 +229,9 @@ describe("app deploy presenter", () => { }); it("keeps verbose-only deploy details out of JSON serialization", () => { - const json = JSON.parse(JSON.stringify(serializeAppDeploy(createDeployResult()))); + const json = JSON.parse( + JSON.stringify(serializeAppDeploy(createDeployResult())), + ); expect(json.deploySettings).toEqual({ config: { diff --git a/packages/cli/tests/app-provider.test.ts b/packages/cli/tests/app-provider.test.ts index ec44570..ea29f7b 100644 --- a/packages/cli/tests/app-provider.test.ts +++ b/packages/cli/tests/app-provider.test.ts @@ -20,12 +20,14 @@ describe("preview app provider", () => { const client = { GET: vi.fn().mockResolvedValue({ data: { - data: [{ - id: "br_production", - gitName: "production", - isDefault: true, - role: "preview", - }], + data: [ + { + id: "br_production", + gitName: "production", + isDefault: true, + role: "preview", + }, + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, @@ -38,12 +40,16 @@ describe("preview app provider", () => { ComputeClient: class {}, })); - const { createPreviewAppProvider } = await import("../src/lib/app/preview-provider"); + const { createPreviewAppProvider } = await import( + "../src/lib/app/preview-provider" + ); const provider = createPreviewAppProvider(client as never); - await expect(provider.resolveBranch("proj_123", { - branchName: "production", - })).resolves.toEqual({ + await expect( + provider.resolveBranch("proj_123", { + branchName: "production", + }), + ).resolves.toEqual({ id: "br_production", name: "production", role: "preview", @@ -77,7 +83,9 @@ describe("preview app provider", () => { }, })); - const { createPreviewAppProvider } = await import("../src/lib/app/preview-provider"); + const { createPreviewAppProvider } = await import( + "../src/lib/app/preview-provider" + ); const provider = createPreviewAppProvider({} as never); const cwd = path.resolve("/tmp/next-smoke"); @@ -174,7 +182,9 @@ describe("preview app provider", () => { }, })); - const { createPreviewAppProvider } = await import("../src/lib/app/preview-provider"); + const { createPreviewAppProvider } = await import( + "../src/lib/app/preview-provider" + ); const provider = createPreviewAppProvider(client as never); const cwd = path.resolve("/tmp/next-smoke"); @@ -245,12 +255,14 @@ describe("preview app provider", () => { if (pathName === "/v1/projects/{projectId}/branches") { return { data: { - data: [{ - id: "br_billing", - gitName: "feat/billing", - isDefault: false, - role: "preview", - }], + data: [ + { + id: "br_billing", + gitName: "feat/billing", + isDefault: false, + role: "preview", + }, + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, @@ -260,15 +272,17 @@ describe("preview app provider", () => { if (pathName === "/v1/compute-services") { return { data: { - data: [{ - id: "svc_branch", - name: "hello-world", - region: { id: "eu-central-1", name: "Europe (Frankfurt)" }, - projectId: "proj_123", - branchId: "br_billing", - latestVersionId: null, - serviceEndpointDomain: "hello-world.fra.prisma.build", - }], + data: [ + { + id: "svc_branch", + name: "hello-world", + region: { id: "eu-central-1", name: "Europe (Frankfurt)" }, + projectId: "proj_123", + branchId: "br_billing", + latestVersionId: null, + serviceEndpointDomain: "hello-world.fra.prisma.build", + }, + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, @@ -304,7 +318,9 @@ describe("preview app provider", () => { }, })); - const { createPreviewAppProvider } = await import("../src/lib/app/preview-provider"); + const { createPreviewAppProvider } = await import( + "../src/lib/app/preview-provider" + ); const provider = createPreviewAppProvider(client as never); const cwd = path.resolve("/tmp/next-smoke"); @@ -356,20 +372,22 @@ describe("preview app provider", () => { if (pathName === "/v1/compute-services/{computeServiceId}/domains") { return { data: { - data: [{ - id: "dom_123", - type: "custom-domain", - url: "https://api.prisma.io/v1/domains/dom_123", - hostname: "shop.acme.com", - computeServiceId: "app_1", - status: "active", - foundryStatus: "active", - failureReason: null, - failureCategory: null, - certExpiresAt: null, - createdAt: "2026-05-22T09:14:00.000Z", - updatedAt: "2026-05-22T09:14:00.000Z", - }], + data: [ + { + id: "dom_123", + type: "custom-domain", + url: "https://api.prisma.io/v1/domains/dom_123", + hostname: "shop.acme.com", + computeServiceId: "app_1", + status: "active", + foundryStatus: "active", + failureReason: null, + failureCategory: null, + certExpiresAt: null, + createdAt: "2026-05-22T09:14:00.000Z", + updatedAt: "2026-05-22T09:14:00.000Z", + }, + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, @@ -400,7 +418,9 @@ describe("preview app provider", () => { ComputeClient: class {}, })); - const { createPreviewAppProvider } = await import("../src/lib/app/preview-provider"); + const { createPreviewAppProvider } = await import( + "../src/lib/app/preview-provider" + ); const provider = createPreviewAppProvider(client as never); const result = await provider.addDomain({ @@ -443,20 +463,22 @@ describe("preview app provider", () => { if (pathName === "/v1/compute-services/{computeServiceId}/domains") { return { data: { - data: [{ - id: "dom_123", - type: "custom-domain", - url: "https://api.prisma.io/v1/domains/dom_123", - hostname: "other.acme.com", - computeServiceId: "app_1", - status: "active", - foundryStatus: "active", - failureReason: null, - failureCategory: null, - certExpiresAt: null, - createdAt: "2026-05-22T09:14:00.000Z", - updatedAt: "2026-05-22T09:14:00.000Z", - }], + data: [ + { + id: "dom_123", + type: "custom-domain", + url: "https://api.prisma.io/v1/domains/dom_123", + hostname: "other.acme.com", + computeServiceId: "app_1", + status: "active", + foundryStatus: "active", + failureReason: null, + failureCategory: null, + certExpiresAt: null, + createdAt: "2026-05-22T09:14:00.000Z", + updatedAt: "2026-05-22T09:14:00.000Z", + }, + ], pagination: { hasMore: false, nextCursor: null }, }, response: { status: 200 }, @@ -487,14 +509,18 @@ describe("preview app provider", () => { ComputeClient: class {}, })); - const { createPreviewAppProvider } = await import("../src/lib/app/preview-provider"); + const { createPreviewAppProvider } = await import( + "../src/lib/app/preview-provider" + ); const provider = createPreviewAppProvider(client as never); - await expect(provider.addDomain({ - appId: "app_1", - hostname: "shop.acme.com", - })).rejects.toMatchObject({ + await expect( + provider.addDomain({ + appId: "app_1", + hostname: "shop.acme.com", + }), + ).rejects.toMatchObject({ status: 409, code: "CONFLICT", }); diff --git a/packages/cli/tests/app-state.test.ts b/packages/cli/tests/app-state.test.ts index dba8e6e..2441417 100644 --- a/packages/cli/tests/app-state.test.ts +++ b/packages/cli/tests/app-state.test.ts @@ -18,7 +18,12 @@ describe("app local state", () => { }); expect( - JSON.parse(await readFile(path.join(cwd, DEFAULT_STATE_DIR_NAME, "state.json"), "utf8")), + JSON.parse( + await readFile( + path.join(cwd, DEFAULT_STATE_DIR_NAME, "state.json"), + "utf8", + ), + ), ).toMatchObject({ app: { selectedByProject: { @@ -59,7 +64,10 @@ describe("app local state", () => { const controller = new AbortController(); const reason = new Error("cancelled"); controller.abort(reason); - const store = new LocalStateStore(path.join(cwd, DEFAULT_STATE_DIR_NAME), controller.signal); + const store = new LocalStateStore( + path.join(cwd, DEFAULT_STATE_DIR_NAME), + controller.signal, + ); await expect(store.read()).rejects.toBe(reason); }); @@ -72,7 +80,12 @@ describe("app local state", () => { await store.setKnownLiveDeployment("proj_123", "app_456", "dep_456"); expect( - JSON.parse(await readFile(path.join(cwd, DEFAULT_STATE_DIR_NAME, "state.json"), "utf8")), + JSON.parse( + await readFile( + path.join(cwd, DEFAULT_STATE_DIR_NAME, "state.json"), + "utf8", + ), + ), ).toMatchObject({ app: { knownLiveDeploymentByProject: { @@ -83,8 +96,12 @@ describe("app local state", () => { }, }, }); - await expect(store.readKnownLiveDeployment("proj_123", "app_123")).resolves.toBe("dep_123"); - await expect(store.readKnownLiveDeployment("proj_123", "app_456")).resolves.toBe("dep_456"); + await expect( + store.readKnownLiveDeployment("proj_123", "app_123"), + ).resolves.toBe("dep_123"); + await expect( + store.readKnownLiveDeployment("proj_123", "app_456"), + ).resolves.toBe("dep_456"); }); it("clears the selected app only when the deleted app matches", async () => { @@ -118,7 +135,11 @@ describe("app local state", () => { await store.clearKnownLiveDeployment("proj_123", "app_123"); - await expect(store.readKnownLiveDeployment("proj_123", "app_123")).resolves.toBeNull(); - await expect(store.readKnownLiveDeployment("proj_123", "app_456")).resolves.toBe("dep_456"); + await expect( + store.readKnownLiveDeployment("proj_123", "app_123"), + ).resolves.toBeNull(); + await expect( + store.readKnownLiveDeployment("proj_123", "app_456"), + ).resolves.toBe("dep_456"); }); }); diff --git a/packages/cli/tests/app.test.ts b/packages/cli/tests/app.test.ts index 9f8ebda..c6fd52a 100644 --- a/packages/cli/tests/app.test.ts +++ b/packages/cli/tests/app.test.ts @@ -130,31 +130,51 @@ describe("app commands", () => { expect(rootHelp.stderr).toContain("app"); expect(appHelp.exitCode).toBe(0); - expect(appHelp.stderr).toContain("Manage apps and deployments for a project"); + expect(appHelp.stderr).toContain( + "Manage apps and deployments for a project", + ); expect(appHelp.stderr).toContain("$ prisma-cli app deploy"); - expect(appHelp.stderr).toContain("$ prisma-cli app deploy --app my-app --framework nextjs --http-port 3000"); + expect(appHelp.stderr).toContain( + "$ prisma-cli app deploy --app my-app --framework nextjs --http-port 3000", + ); expect(appHelp.stderr).not.toContain("update-env"); expect(appHelp.stderr).not.toContain("list-env"); expect(buildHelp.exitCode).toBe(0); - expect(buildHelp.stderr).toContain("Build the app locally into a deployable artifact"); - expect(buildHelp.stderr).toContain("$ prisma-cli app build --build-type nextjs"); + expect(buildHelp.stderr).toContain( + "Build the app locally into a deployable artifact", + ); + expect(buildHelp.stderr).toContain( + "$ prisma-cli app build --build-type nextjs", + ); expect(runHelp.exitCode).toBe(0); expect(runHelp.stderr).toContain("Run your app locally"); - expect(runHelp.stderr).toContain("$ prisma-cli app run --build-type nextjs"); + expect(runHelp.stderr).toContain( + "$ prisma-cli app run --build-type nextjs", + ); expect(deployHelp.exitCode).toBe(0); expect(deployHelp.stderr).toContain("Creates a new deployment for the app"); - expect(deployHelp.stderr).toContain("Agent skills for guided Next.js deploys"); + expect(deployHelp.stderr).toContain( + "Agent skills for guided Next.js deploys", + ); expect(deployHelp.stderr).toContain("$ prisma-cli app deploy"); - expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --project proj_123"); - expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --create-project my-app --yes"); + expect(deployHelp.stderr).toContain( + "$ prisma-cli app deploy --project proj_123", + ); + expect(deployHelp.stderr).toContain( + "$ prisma-cli app deploy --create-project my-app --yes", + ); expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --env .env"); expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --db"); expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --db --yes"); - expect(deployHelp.stderr).toContain("$ prisma-cli app deploy --app my-app --framework nextjs --http-port 3000"); - expect(deployHelp.stderr).toContain("$ pnpm dlx skills@latest add prisma/prisma-cli/skills#cli-v --all"); + expect(deployHelp.stderr).toContain( + "$ prisma-cli app deploy --app my-app --framework nextjs --http-port 3000", + ); + expect(deployHelp.stderr).toContain( + "$ pnpm dlx skills@latest add prisma/prisma-cli/skills#cli-v --all", + ); expect(deployHelp.stderr).toContain("--entry "); expect(deployHelp.stderr).toContain("--create-project "); expect(deployHelp.stderr).toContain("--framework "); @@ -165,7 +185,9 @@ describe("app commands", () => { expect(deployHelp.stderr).toContain("--no-db"); expect(showHelp.exitCode).toBe(0); - expect(showHelp.stderr).toContain("Show the app and its current deployment"); + expect(showHelp.stderr).toContain( + "Show the app and its current deployment", + ); expect(showHelp.stderr).toContain("$ prisma-cli app show"); expect(openHelp.exitCode).toBe(0); @@ -174,28 +196,44 @@ describe("app commands", () => { expect(domainHelp.exitCode).toBe(0); expect(domainHelp.stderr).toContain("Manage custom domains for an app"); - expect(domainHelp.stderr).toContain("$ prisma-cli app domain add shop.acme.com"); + expect(domainHelp.stderr).toContain( + "$ prisma-cli app domain add shop.acme.com", + ); expect(domainAddHelp.exitCode).toBe(0); - expect(domainAddHelp.stderr).toContain("Register a custom domain on the app's production branch"); + expect(domainAddHelp.stderr).toContain( + "Register a custom domain on the app's production branch", + ); expect(domainAddHelp.stderr).toContain("--branch "); expect(domainShowHelp.exitCode).toBe(0); - expect(domainShowHelp.stderr).toContain("Show custom domain status and certificate details"); + expect(domainShowHelp.stderr).toContain( + "Show custom domain status and certificate details", + ); expect(domainRemoveHelp.exitCode).toBe(0); - expect(domainRemoveHelp.stderr).toContain("Detach a custom domain from the app"); + expect(domainRemoveHelp.stderr).toContain( + "Detach a custom domain from the app", + ); expect(domainRetryHelp.exitCode).toBe(0); - expect(domainRetryHelp.stderr).toContain("Retry custom domain DNS verification and TLS provisioning"); + expect(domainRetryHelp.stderr).toContain( + "Retry custom domain DNS verification and TLS provisioning", + ); expect(domainWaitHelp.exitCode).toBe(0); - expect(domainWaitHelp.stderr).toContain("Wait until a custom domain is active or failed"); + expect(domainWaitHelp.stderr).toContain( + "Wait until a custom domain is active or failed", + ); expect(domainWaitHelp.stderr).toContain("--timeout "); expect(logsHelp.exitCode).toBe(0); - expect(logsHelp.stderr).toContain("Stream logs for the app's current deployment"); - expect(logsHelp.stderr).toContain("$ prisma-cli app logs --deployment dep_123"); + expect(logsHelp.stderr).toContain( + "Stream logs for the app's current deployment", + ); + expect(logsHelp.stderr).toContain( + "$ prisma-cli app logs --deployment dep_123", + ); expect(listDeploysHelp.exitCode).toBe(0); expect(listDeploysHelp.stderr).toContain("List deployments for the app"); @@ -203,19 +241,29 @@ describe("app commands", () => { expect(showDeployHelp.exitCode).toBe(0); expect(showDeployHelp.stderr).toContain("Show a deployment in detail"); - expect(showDeployHelp.stderr).toContain("$ prisma-cli app show-deploy dep_123"); + expect(showDeployHelp.stderr).toContain( + "$ prisma-cli app show-deploy dep_123", + ); expect(promoteHelp.exitCode).toBe(0); - expect(promoteHelp.stderr).toContain("Promote a deployment to production by rebuilding with production env vars"); + expect(promoteHelp.stderr).toContain( + "Promote a deployment to production by rebuilding with production env vars", + ); expect(promoteHelp.stderr).toContain("$ prisma-cli app promote dep_123"); expect(rollbackHelp.exitCode).toBe(0); - expect(rollbackHelp.stderr).toContain("Roll back production to a previous deployment"); + expect(rollbackHelp.stderr).toContain( + "Roll back production to a previous deployment", + ); expect(rollbackHelp.stderr).toContain("$ prisma-cli app rollback"); expect(removeHelp.exitCode).toBe(0); - expect(removeHelp.stderr).toContain("Remove the app from the current branch"); - expect(removeHelp.stderr).toContain("$ prisma-cli app remove --app hello-world"); + expect(removeHelp.stderr).toContain( + "Remove the app from the current branch", + ); + expect(removeHelp.stderr).toContain( + "$ prisma-cli app remove --app hello-world", + ); }); it("does not register legacy app env commands", async () => { @@ -253,6 +301,8 @@ describe("app commands", () => { }); expect(result.exitCode).toBe(2); - expect(result.stderr).toContain("app deploy accepts either --db or --no-db"); + expect(result.stderr).toContain( + "app deploy accepts either --db or --no-db", + ); }); }); diff --git a/packages/cli/tests/auth-login.test.ts b/packages/cli/tests/auth-login.test.ts index 4cb60d5..8187497 100644 --- a/packages/cli/tests/auth-login.test.ts +++ b/packages/cli/tests/auth-login.test.ts @@ -94,14 +94,16 @@ describe("auth login callback", () => { const { login } = await import("../src/lib/auth/login"); - await expect(login({ - hostname: "127.0.0.1", - tokenStorage, - signal: controller.signal, - openUrl: () => { - controller.abort(reason); - }, - })).rejects.toBe(reason); + await expect( + login({ + hostname: "127.0.0.1", + tokenStorage, + signal: controller.signal, + openUrl: () => { + controller.abort(reason); + }, + }), + ).rejects.toBe(reason); }); it("rejects when the command signal aborts during workspace lookup", async () => { @@ -120,37 +122,41 @@ describe("auth login callback", () => { vi.doMock("@prisma/management-api-sdk", () => ({ AuthError: class SDKAuthError extends Error {}, - createManagementApiSdk: vi.fn().mockImplementation((sdkOptions: { redirectUri: string }) => { - redirectUri = sdkOptions.redirectUri; - - return { - getLoginUrl: vi.fn().mockReturnValue({ - url: "https://auth.example.test/login", - state: "state_123", - verifier: "verifier_123", - }), - handleCallback: vi.fn().mockResolvedValue(undefined), - client: { - GET: vi.fn().mockImplementation(() => { - controller.abort(reason); - throw reason; + createManagementApiSdk: vi + .fn() + .mockImplementation((sdkOptions: { redirectUri: string }) => { + redirectUri = sdkOptions.redirectUri; + + return { + getLoginUrl: vi.fn().mockReturnValue({ + url: "https://auth.example.test/login", + state: "state_123", + verifier: "verifier_123", }), - }, - }; - }), + handleCallback: vi.fn().mockResolvedValue(undefined), + client: { + GET: vi.fn().mockImplementation(() => { + controller.abort(reason); + throw reason; + }), + }, + }; + }), })); const { login } = await import("../src/lib/auth/login"); - await expect(login({ - hostname: "127.0.0.1", - tokenStorage, - signal: controller.signal, - openUrl: async () => { - expect(redirectUri).toBeDefined(); - await fetch(`${redirectUri}?code=code_123&state=state_123`); - }, - })).rejects.toBe(reason); + await expect( + login({ + hostname: "127.0.0.1", + tokenStorage, + signal: controller.signal, + openUrl: async () => { + expect(redirectUri).toBeDefined(); + await fetch(`${redirectUri}?code=code_123&state=state_123`); + }, + }), + ).rejects.toBe(reason); }); }); diff --git a/packages/cli/tests/auth-ops.test.ts b/packages/cli/tests/auth-ops.test.ts index aefe69d..69f667d 100644 --- a/packages/cli/tests/auth-ops.test.ts +++ b/packages/cli/tests/auth-ops.test.ts @@ -8,7 +8,9 @@ afterEach(() => { }); function encodeJwt(claims: Record): string { - const payload = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url"); + const payload = Buffer.from(JSON.stringify(claims), "utf8").toString( + "base64url", + ); return `header.${payload}.signature`; } @@ -91,20 +93,30 @@ describe("readAuthState", () => { refreshToken: "refresh-token", }); const requireComputeAuth = vi.fn().mockResolvedValue({ - GET: vi.fn().mockImplementation((pathName: string, request?: { params?: { path?: { id?: string } } }) => { - if (pathName === "/v1/workspaces/{id}" && request?.params?.path?.id === "cmmxlp7ae1251zyfs8mdpnavm") { - return { - data: { - data: { - id: "wksp_cmmxlp7ae1251zyfs8mdpnavm", - name: "Sandpit", - }, - }, - }; - } + GET: vi + .fn() + .mockImplementation( + ( + pathName: string, + request?: { params?: { path?: { id?: string } } }, + ) => { + if ( + pathName === "/v1/workspaces/{id}" && + request?.params?.path?.id === "cmmxlp7ae1251zyfs8mdpnavm" + ) { + return { + data: { + data: { + id: "wksp_cmmxlp7ae1251zyfs8mdpnavm", + name: "Sandpit", + }, + }, + }; + } - throw new Error(`Unexpected path ${pathName}`); - }), + throw new Error(`Unexpected path ${pathName}`); + }, + ), }); vi.doMock("../src/adapters/token-storage", () => ({ @@ -157,14 +169,16 @@ describe("readAuthState", () => { const { readAuthState } = await import("../src/lib/auth/auth-ops"); - await expect(readAuthState({} as NodeJS.ProcessEnv)).resolves.toMatchObject({ - authenticated: true, - user: null, - workspace: { - id: "wksp_cmmxlp7ae1251zyfs8mdpnavm", - name: "Sandpit", + await expect(readAuthState({} as NodeJS.ProcessEnv)).resolves.toMatchObject( + { + authenticated: true, + user: null, + workspace: { + id: "wksp_cmmxlp7ae1251zyfs8mdpnavm", + name: "Sandpit", + }, }, - }); + ); }); it("uses the canonical workspace id as the fallback name when the API omits a name", async () => { @@ -192,50 +206,62 @@ describe("readAuthState", () => { const { readAuthState } = await import("../src/lib/auth/auth-ops"); - await expect(readAuthState({} as NodeJS.ProcessEnv)).resolves.toMatchObject({ - workspace: { - id: "wksp_cmmxlp7ae1251zyfs8mdpnavm", - name: "wksp_cmmxlp7ae1251zyfs8mdpnavm", + await expect(readAuthState({} as NodeJS.ProcessEnv)).resolves.toMatchObject( + { + workspace: { + id: "wksp_cmmxlp7ae1251zyfs8mdpnavm", + name: "wksp_cmmxlp7ae1251zyfs8mdpnavm", + }, }, - }); + ); }); it("derives authenticated state from PRISMA_SERVICE_TOKEN without consulting FileTokenStorage", async () => { const getTokens = vi.fn(); const requireComputeAuth = vi.fn().mockResolvedValue({ - GET: vi.fn().mockImplementation((pathName: string, request?: { params?: { path?: { id?: string } } }) => { - if (pathName === "/v1/me") { - return { - data: { - data: { - user: null, - workspace: { - id: "wksp_clitq5hfg0000qv0gtg9nv9fy", - name: "Prisma Platform", + GET: vi + .fn() + .mockImplementation( + ( + pathName: string, + request?: { params?: { path?: { id?: string } } }, + ) => { + if (pathName === "/v1/me") { + return { + data: { + data: { + user: null, + workspace: { + id: "wksp_clitq5hfg0000qv0gtg9nv9fy", + name: "Prisma Platform", + }, + credential: { + type: "service_token", + id: "itgr_ci", + name: "ci-deploys-prod", + }, + }, }, - credential: { - type: "service_token", - id: "itgr_ci", - name: "ci-deploys-prod", + }; + } + + if ( + pathName === "/v1/workspaces/{id}" && + request?.params?.path?.id === "clitq5hfg0000qv0gtg9nv9fy" + ) { + return { + data: { + data: { + id: "wksp_clitq5hfg0000qv0gtg9nv9fy", + name: "Prisma Platform", + }, }, - }, - }, - }; - } - - if (pathName === "/v1/workspaces/{id}" && request?.params?.path?.id === "clitq5hfg0000qv0gtg9nv9fy") { - return { - data: { - data: { - id: "wksp_clitq5hfg0000qv0gtg9nv9fy", - name: "Prisma Platform", - }, - }, - }; - } + }; + } - throw new Error(`Unexpected path ${pathName}`); - }), + throw new Error(`Unexpected path ${pathName}`); + }, + ), }); vi.doMock("../src/adapters/token-storage", () => ({ @@ -246,7 +272,10 @@ describe("readAuthState", () => { })); const { readAuthState } = await import("../src/lib/auth/auth-ops"); - const token = encodeJwt({ sub: "workspace:clitq5hfg0000qv0gtg9nv9fy", email: "service@example.com" }); + const token = encodeJwt({ + sub: "workspace:clitq5hfg0000qv0gtg9nv9fy", + email: "service@example.com", + }); await expect( readAuthState({ PRISMA_SERVICE_TOKEN: token } as NodeJS.ProcessEnv), @@ -275,13 +304,19 @@ describe("readAuthState", () => { // into. const getTokens = vi.fn().mockResolvedValue({ workspaceId: "wksp_local_oauth_workspace", - accessToken: encodeJwt({ sub: "user:usr_local", email: "dev@example.com" }), + accessToken: encodeJwt({ + sub: "user:usr_local", + email: "dev@example.com", + }), refreshToken: "refresh-token", }); const requireComputeAuth = vi.fn().mockResolvedValue({ GET: vi.fn().mockResolvedValue({ data: { - data: { id: "wksp_clitq5hfg0000qv0gtg9nv9fy", name: "Prisma Platform" }, + data: { + id: "wksp_clitq5hfg0000qv0gtg9nv9fy", + name: "Prisma Platform", + }, }, }), }); @@ -294,7 +329,9 @@ describe("readAuthState", () => { const { readAuthState } = await import("../src/lib/auth/auth-ops"); const token = encodeJwt({ sub: "workspace:clitq5hfg0000qv0gtg9nv9fy" }); - const result = await readAuthState({ PRISMA_SERVICE_TOKEN: token } as NodeJS.ProcessEnv); + const result = await readAuthState({ + PRISMA_SERVICE_TOKEN: token, + } as NodeJS.ProcessEnv); expect(result.authenticated).toBe(true); expect(result.workspace?.id).toBe("wksp_clitq5hfg0000qv0gtg9nv9fy"); @@ -418,7 +455,10 @@ describe("readAuthState", () => { const token = encodeJwt({ sub: "workspace:clitq5hfg0000qv0gtg9nv9fy" }); await expect( - readAuthState({ PRISMA_SERVICE_TOKEN: token } as NodeJS.ProcessEnv, controller.signal), + readAuthState( + { PRISMA_SERVICE_TOKEN: token } as NodeJS.ProcessEnv, + controller.signal, + ), ).rejects.toBe(reason); }); @@ -447,7 +487,10 @@ describe("readAuthState", () => { const token = encodeJwt({ sub: "workspace:clitq5hfg0000qv0gtg9nv9fy" }); await expect( - readAuthState({ PRISMA_SERVICE_TOKEN: token } as NodeJS.ProcessEnv, controller.signal), + readAuthState( + { PRISMA_SERVICE_TOKEN: token } as NodeJS.ProcessEnv, + controller.signal, + ), ).rejects.toBe(reason); }); diff --git a/packages/cli/tests/auth-real-mode.test.ts b/packages/cli/tests/auth-real-mode.test.ts index 10de7f1..0e455ad 100644 --- a/packages/cli/tests/auth-real-mode.test.ts +++ b/packages/cli/tests/auth-real-mode.test.ts @@ -37,7 +37,9 @@ describe("real auth mode", () => { performLogout, })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAuthLogin } = await import("../src/controllers/auth"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -52,8 +54,14 @@ describe("real auth mode", () => { const result = await runAuthLogin(context, {}); - expect(performLogin).toHaveBeenCalledWith(context.runtime.env, context.runtime.signal); - expect(readAuthState).toHaveBeenCalledWith(context.runtime.env, context.runtime.signal); + expect(performLogin).toHaveBeenCalledWith( + context.runtime.env, + context.runtime.signal, + ); + expect(readAuthState).toHaveBeenCalledWith( + context.runtime.env, + context.runtime.signal, + ); expect(result.result).toMatchObject({ authenticated: true, provider: null, @@ -74,7 +82,9 @@ describe("real auth mode", () => { performLogout, })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runAuthLogin } = await import("../src/controllers/auth"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -102,7 +112,9 @@ describe("real auth mode", () => { it("does not eagerly load fixtures in real mode", async () => { const loadSpy = vi.spyOn(MockApi, "load"); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); const { context } = await createTestCommandContext({ @@ -182,7 +194,9 @@ describe("real auth mode", () => { }); it("omits empty provider and workspace rows in auth output", async () => { - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); const { context } = await createTestCommandContext({ @@ -214,7 +228,9 @@ describe("real auth mode", () => { }); it("omits the user row when a real auth state has no email", async () => { - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); const { context } = await createTestCommandContext({ @@ -247,7 +263,9 @@ describe("real auth mode", () => { }); it("shows service-token identity when no human user is present", async () => { - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); const { context } = await createTestCommandContext({ diff --git a/packages/cli/tests/auth.test.ts b/packages/cli/tests/auth.test.ts index 9f6ab97..bd0e4d1 100644 --- a/packages/cli/tests/auth.test.ts +++ b/packages/cli/tests/auth.test.ts @@ -160,7 +160,9 @@ describe("auth commands", () => { const stderr = stripAnsi(result.stderr); expect(result.exitCode).toBe(0); - expect(stderr).toContain("auth whoami → Showing the current authenticated identity."); + expect(stderr).toContain( + "auth whoami → Showing the current authenticated identity.", + ); expect(stderr).not.toContain("Read more"); expect(stderr).toContain("status: signed out"); }); diff --git a/packages/cli/tests/branch-controller.test.ts b/packages/cli/tests/branch-controller.test.ts index 75dbcc0..778d88d 100644 --- a/packages/cli/tests/branch-controller.test.ts +++ b/packages/cli/tests/branch-controller.test.ts @@ -14,50 +14,61 @@ afterEach(() => { function createMockClient() { return { - GET: vi.fn().mockImplementation((pathName: string, request?: { params?: { query?: { cursor?: string } } }) => { - if (pathName === "/v1/projects") { - return { - data: { - data: [ - { - id: "proj_123", - name: "Acme Dashboard", - slug: "acme-dashboard", - workspace: { id: "ws_123", name: "Acme Inc" }, + GET: vi + .fn() + .mockImplementation( + ( + pathName: string, + request?: { params?: { query?: { cursor?: string } } }, + ) => { + if (pathName === "/v1/projects") { + return { + data: { + data: [ + { + id: "proj_123", + name: "Acme Dashboard", + slug: "acme-dashboard", + workspace: { id: "ws_123", name: "Acme Inc" }, + }, + ], }, - ], - }, - response: { status: 200 }, - }; - } + response: { status: 200 }, + }; + } - if (pathName === "/v1/projects/{projectId}/branches") { - const cursor = request?.params?.query?.cursor; - if (cursor === "cursor_2") { - return { - data: { - data: [ - { id: "br_main", gitName: "main", role: "production" }, - ], - pagination: { hasMore: false, nextCursor: null }, - }, - response: { status: 200 }, - }; - } + if (pathName === "/v1/projects/{projectId}/branches") { + const cursor = request?.params?.query?.cursor; + if (cursor === "cursor_2") { + return { + data: { + data: [ + { id: "br_main", gitName: "main", role: "production" }, + ], + pagination: { hasMore: false, nextCursor: null }, + }, + response: { status: 200 }, + }; + } - return { - data: { - data: [ - { id: "br_feature", gitName: "feature/auth", role: "preview" }, - ], - pagination: { hasMore: true, nextCursor: "cursor_2" }, - }, - response: { status: 200 }, - }; - } + return { + data: { + data: [ + { + id: "br_feature", + gitName: "feature/auth", + role: "preview", + }, + ], + pagination: { hasMore: true, nextCursor: "cursor_2" }, + }, + response: { status: 200 }, + }; + } - throw new Error(`Unexpected path ${pathName}`); - }), + throw new Error(`Unexpected path ${pathName}`); + }, + ), }; } @@ -129,7 +140,10 @@ describe("branch controller", () => { expect(client.GET).toHaveBeenCalledWith( "/v1/projects/{projectId}/branches", expect.objectContaining({ - params: { path: { projectId: "proj_123" }, query: { cursor: "cursor_2" } }, + params: { + path: { projectId: "proj_123" }, + query: { cursor: "cursor_2" }, + }, }), ); expect(result).toEqual({ @@ -139,8 +153,18 @@ describe("branch controller", () => { projectName: "Acme Dashboard", verboseContext: expectedBranchVerboseContext(), branches: [ - { id: "br_main", name: "main", role: "production", envMap: "production" }, - { id: "br_feature", name: "feature/auth", role: "preview", envMap: "preview" }, + { + id: "br_main", + name: "main", + role: "production", + envMap: "production", + }, + { + id: "br_feature", + name: "feature/auth", + role: "preview", + envMap: "preview", + }, ], }, warnings: [], diff --git a/packages/cli/tests/branch.test.ts b/packages/cli/tests/branch.test.ts index df333e0..6e01f15 100644 --- a/packages/cli/tests/branch.test.ts +++ b/packages/cli/tests/branch.test.ts @@ -71,10 +71,8 @@ describe("branch commands", () => { flags: { verbose: true }, }); - const output = stripAnsi(renderBranchList( - context, - getCommandDescriptor("branch.list"), - { + const output = stripAnsi( + renderBranchList(context, getCommandDescriptor("branch.list"), { projectId: "proj_empty", projectName: "Empty Project", branches: [], @@ -91,8 +89,8 @@ describe("branch commands", () => { projectSource: "explicit", }, }, - }, - ).join("\n")); + }).join("\n"), + ); expect(output).toContain("No branches found."); expect(output).toContain("Resolved context:"); @@ -122,7 +120,12 @@ describe("branch commands", () => { projectId: "proj_123", projectName: "Acme Dashboard", branches: [ - { id: "br_456", name: "production", role: "production", envMap: "production" }, + { + id: "br_456", + name: "production", + role: "production", + envMap: "production", + }, { id: "br_234", name: "pr-123", role: "preview", envMap: "preview" }, { id: "br_123", name: "preview", role: "preview", envMap: "preview" }, { id: "br_345", name: "staging", role: "preview", envMap: "preview" }, @@ -157,7 +160,9 @@ describe("branch commands", () => { expect(branchHelp.stderr).not.toContain("branch use"); expect(listHelp.exitCode).toBe(0); - expect(listHelp.stderr).toContain("List Platform branches for the resolved project"); + expect(listHelp.stderr).toContain( + "List Platform branches for the resolved project", + ); expect(listHelp.stderr).toContain("$ prisma-cli branch list"); expect(listHelp.stderr).toContain("$ prisma-cli branch list --json"); }); diff --git a/packages/cli/tests/command-runner-auth.test.ts b/packages/cli/tests/command-runner-auth.test.ts index 638a560..b971880 100644 --- a/packages/cli/tests/command-runner-auth.test.ts +++ b/packages/cli/tests/command-runner-auth.test.ts @@ -13,7 +13,11 @@ class CaptureStream extends Writable { declare columns?: number; declare rows?: number; - _write(chunk: Buffer | string, _encoding: BufferEncoding, callback: (error?: Error | null) => void) { + _write( + chunk: Buffer | string, + _encoding: BufferEncoding, + callback: (error?: Error | null) => void, + ) { this.buffer += chunk.toString(); callback(); } @@ -87,7 +91,11 @@ describe("command runner auth errors", () => { }); it("renders abort failures as structured CLI cancellation errors", async () => { - const { runtime, stdout } = await createRuntime(["--json", "app", "deploy"]); + const { runtime, stdout } = await createRuntime([ + "--json", + "app", + "deploy", + ]); await runCommand( runtime, @@ -177,7 +185,11 @@ describe("command runner auth errors", () => { }); it("shows SDK auth details only with trace enabled", async () => { - const { runtime, stderr } = await createRuntime(["auth", "whoami", "--trace"]); + const { runtime, stderr } = await createRuntime([ + "auth", + "whoami", + "--trace", + ]); await runCommand( runtime, @@ -196,7 +208,11 @@ describe("command runner auth errors", () => { }); it("renders streaming SDK auth failures as JSON events", async () => { - const { runtime, stdout } = await createRuntime(["--json", "app", "deploy"]); + const { runtime, stdout } = await createRuntime([ + "--json", + "app", + "deploy", + ]); await runStreamingCommand(runtime, "app.deploy", {}, async () => { throw new SDKAuthError("invalid_grant: Invalid grant", true); diff --git a/packages/cli/tests/command-runner.test.ts b/packages/cli/tests/command-runner.test.ts index 21858ad..07f39fe 100644 --- a/packages/cli/tests/command-runner.test.ts +++ b/packages/cli/tests/command-runner.test.ts @@ -16,11 +16,18 @@ class CaptureStream extends Writable { declare columns?: number; declare rows?: number; - constructor(private readonly streamName?: "stdout" | "stderr", private readonly writes?: CapturedWrite[]) { + constructor( + private readonly streamName?: "stdout" | "stderr", + private readonly writes?: CapturedWrite[], + ) { super(); } - _write(chunk: Buffer | string, _encoding: BufferEncoding, callback: (error?: Error | null) => void) { + _write( + chunk: Buffer | string, + _encoding: BufferEncoding, + callback: (error?: Error | null) => void, + ) { const text = chunk.toString(); this.buffer += text; if (this.streamName && this.writes) { @@ -82,7 +89,11 @@ afterEach(() => { describe("command runner success output", () => { it("adds local diagnostics to successful verbose human output", async () => { - const { runtime, stderr } = await createRuntime(["project", "show", "--verbose"]); + const { runtime, stderr } = await createRuntime([ + "project", + "show", + "--verbose", + ]); await runCommand( runtime, @@ -110,7 +121,11 @@ describe("command runner success output", () => { }); it("keeps successful verbose output when post-success diagnostics abort", async () => { - const { runtime, controller, stderr } = await createRuntime(["project", "show", "--verbose"]); + const { runtime, controller, stderr } = await createRuntime([ + "project", + "show", + "--verbose", + ]); await runCommand( runtime, @@ -137,7 +152,12 @@ describe("command runner success output", () => { }); it("does not add local diagnostics to successful JSON output", async () => { - const { runtime, stdout, stderr } = await createRuntime(["project", "show", "--verbose", "--json"]); + const { runtime, stdout, stderr } = await createRuntime([ + "project", + "show", + "--verbose", + "--json", + ]); await runCommand( runtime, @@ -165,7 +185,10 @@ describe("command runner success output", () => { }); it("writes human stderr before raw stdout when both are rendered", async () => { - const { runtime, stdout, stderr, writes } = await createRuntime(["project", "show"]); + const { runtime, stdout, stderr, writes } = await createRuntime([ + "project", + "show", + ]); await runCommand( runtime, @@ -190,7 +213,11 @@ describe("command runner success output", () => { }); it("suppresses human output in quiet mode while preserving raw stdout", async () => { - const { runtime, stdout, stderr, writes } = await createRuntime(["project", "show", "--quiet"]); + const { runtime, stdout, stderr, writes } = await createRuntime([ + "project", + "show", + "--quiet", + ]); let renderHumanCalled = false; await runCommand( @@ -220,7 +247,11 @@ describe("command runner success output", () => { }); it("bypasses raw stdout and human output in JSON mode", async () => { - const { runtime, stdout, stderr } = await createRuntime(["project", "show", "--json"]); + const { runtime, stdout, stderr } = await createRuntime([ + "project", + "show", + "--json", + ]); let renderStdoutCalled = false; let renderHumanCalled = false; diff --git a/packages/cli/tests/database.test.ts b/packages/cli/tests/database.test.ts index 4af07f8..3244121 100644 --- a/packages/cli/tests/database.test.ts +++ b/packages/cli/tests/database.test.ts @@ -58,7 +58,9 @@ describe("database commands", () => { }); expect(root.exitCode).toBe(0); - expect(root.stderr).toContain("database Manage Prisma Postgres databases for a project"); + expect(root.stderr).toContain( + "database Manage Prisma Postgres databases for a project", + ); expect(database.exitCode).toBe(0); const databaseHelp = stripAnsi(database.stderr).replace(/[ \t]+\n/g, "\n"); @@ -90,7 +92,10 @@ describe("database commands", () => { expect(databaseHelp).not.toContain("postgres "); expect(connection.exitCode).toBe(0); - const connectionHelp = stripAnsi(connection.stderr).replace(/[ \t]+\n/g, "\n"); + const connectionHelp = stripAnsi(connection.stderr).replace( + /[ \t]+\n/g, + "\n", + ); expect(connectionHelp).toMatchInlineSnapshot(` "database connection → Manage one-time-view database connection strings @@ -149,7 +154,9 @@ describe("database commands", () => { nextSteps: [], nextActions: [], }); - expect(JSON.stringify(payload)).not.toContain("postgresql://secret-preview"); + expect(JSON.stringify(payload)).not.toContain( + "postgresql://secret-preview", + ); expect(JSON.stringify(payload)).not.toContain("connectionString"); }); @@ -178,7 +185,9 @@ describe("database commands", () => { }, ], }); - expect(JSON.stringify(payload)).not.toContain("postgresql://secret-preview"); + expect(JSON.stringify(payload)).not.toContain( + "postgresql://secret-preview", + ); expect(JSON.stringify(payload)).not.toContain("connectionString"); }); @@ -194,14 +203,18 @@ describe("database commands", () => { expect(result.exitCode).toBe(0); expect(stripAnsi(result.stderr)).toBe( - "Creating database...\n" - + "✔ Created database \"scratch\" in Acme Dashboard / preview.\n" - + " The connection URL below is shown once, so save it now.\n" - + "\n", + "Creating database...\n" + + '✔ Created database "scratch" in Acme Dashboard / preview.\n' + + " The connection URL below is shown once, so save it now.\n" + + "\n", + ); + expect(result.stdout).toBe( + "postgresql://db_1003.example.prisma.io/postgres\n", ); - expect(result.stdout).toBe("postgresql://db_1003.example.prisma.io/postgres\n"); expect(result.stdout).not.toContain("DATABASE_URL="); - expect(`${result.stdout}${result.stderr}`.split("postgresql://db_1003")).toHaveLength(2); + expect( + `${result.stdout}${result.stderr}`.split("postgresql://db_1003"), + ).toHaveLength(2); }); it("omits the branch breadcrumb when creating an unscoped database", async () => { @@ -215,16 +228,29 @@ describe("database commands", () => { }); expect(result.exitCode).toBe(0); - expect(stripAnsi(result.stderr)).toContain("✔ Created database \"scratch\" in Acme Dashboard.\n"); + expect(stripAnsi(result.stderr)).toContain( + '✔ Created database "scratch" in Acme Dashboard.\n', + ); expect(stripAnsi(result.stderr)).not.toContain("Acme Dashboard /"); - expect(result.stdout).toBe("postgresql://db_1003.example.prisma.io/postgres\n"); + expect(result.stdout).toBe( + "postgresql://db_1003.example.prisma.io/postgres\n", + ); }); it("prints database create metadata only in verbose human output", async () => { const { cwd, stateDir } = await setupLinkedProject(); const result = await executeCli({ - argv: ["database", "create", "scratch", "--branch", "preview", "--region", "eu-central-1", "--verbose"], + argv: [ + "database", + "create", + "scratch", + "--branch", + "preview", + "--region", + "eu-central-1", + "--verbose", + ], cwd, stateDir, fixturePath, @@ -232,8 +258,12 @@ describe("database commands", () => { const stderr = stripAnsi(result.stderr); expect(result.exitCode).toBe(0); - expect(result.stdout).toBe("postgresql://db_1003.example.prisma.io/postgres\n"); - expect(stderr).toContain("✔ Created database \"scratch\" in Acme Dashboard / preview.\n"); + expect(result.stdout).toBe( + "postgresql://db_1003.example.prisma.io/postgres\n", + ); + expect(stderr).toContain( + '✔ Created database "scratch" in Acme Dashboard / preview.\n', + ); expect(stderr).toContain(" workspace Acme Inc\n"); expect(stderr).toContain(" project Acme Dashboard\n"); expect(stderr).toContain(" branch preview\n"); @@ -257,7 +287,9 @@ describe("database commands", () => { expect(result.exitCode).toBe(0); expect(result.stderr).toBe(""); - expect(result.stdout).toBe("postgresql://db_1003.example.prisma.io/postgres\n"); + expect(result.stdout).toBe( + "postgresql://db_1003.example.prisma.io/postgres\n", + ); }); it("returns the one-time database connection URL once in JSON", async () => { @@ -283,7 +315,9 @@ describe("database commands", () => { connectionString: "postgresql://db_1003.example.prisma.io/postgres", }, }); - expect(JSON.stringify(payload).split("postgresql://db_1003")).toHaveLength(2); + expect(JSON.stringify(payload).split("postgresql://db_1003")).toHaveLength( + 2, + ); }); it("prints exactly one raw connection URL when creating a database connection", async () => { @@ -298,23 +332,35 @@ describe("database commands", () => { expect(result.exitCode).toBe(0); expect(stripAnsi(result.stderr)).toBe( - "Creating connection...\n" - + "✔ Added a connection to \"acme-preview\" in Acme Dashboard / preview.\n" - + " The connection URL below is shown once, so save it now.\n" - + "\n", + "Creating connection...\n" + + '✔ Added a connection to "acme-preview" in Acme Dashboard / preview.\n' + + " The connection URL below is shown once, so save it now.\n" + + "\n", + ); + expect(result.stdout).toBe( + "postgresql://db_123-3.example.prisma.io/postgres\n", ); - expect(result.stdout).toBe("postgresql://db_123-3.example.prisma.io/postgres\n"); expect(result.stdout).not.toContain("DATABASE_URL="); expect(stripAnsi(result.stderr)).not.toContain("cli-"); expect(stripAnsi(result.stderr)).not.toContain("conn_"); - expect(`${result.stdout}${result.stderr}`.split("postgresql://db_123-3")).toHaveLength(2); + expect( + `${result.stdout}${result.stderr}`.split("postgresql://db_123-3"), + ).toHaveLength(2); }); it("prints database connection create metadata only in verbose human output", async () => { const { cwd, stateDir } = await setupLinkedProject(); const result = await executeCli({ - argv: ["database", "connection", "create", "db_123", "--name", "readonly", "--verbose"], + argv: [ + "database", + "connection", + "create", + "db_123", + "--name", + "readonly", + "--verbose", + ], cwd, stateDir, fixturePath, @@ -322,8 +368,12 @@ describe("database commands", () => { const stderr = stripAnsi(result.stderr); expect(result.exitCode).toBe(0); - expect(result.stdout).toBe("postgresql://db_123-3.example.prisma.io/postgres\n"); - expect(stderr).toContain("✔ Added a connection to \"acme-preview\" in Acme Dashboard / preview.\n"); + expect(result.stdout).toBe( + "postgresql://db_123-3.example.prisma.io/postgres\n", + ); + expect(stderr).toContain( + '✔ Added a connection to "acme-preview" in Acme Dashboard / preview.\n', + ); expect(stderr).toContain(" workspace Acme Inc\n"); expect(stderr).toContain(" project Acme Dashboard\n"); expect(stderr).toContain(" branch preview\n"); @@ -345,7 +395,9 @@ describe("database commands", () => { expect(result.exitCode).toBe(0); expect(result.stderr).toBe(""); - expect(result.stdout).toBe("postgresql://db_123-3.example.prisma.io/postgres\n"); + expect(result.stdout).toBe( + "postgresql://db_123-3.example.prisma.io/postgres\n", + ); }); it("returns the one-time database connection URL once in JSON", async () => { @@ -371,14 +423,23 @@ describe("database commands", () => { connectionString: "postgresql://db_123-3.example.prisma.io/postgres", }, }); - expect(JSON.stringify(payload).split("postgresql://db_123-3")).toHaveLength(2); + expect(JSON.stringify(payload).split("postgresql://db_123-3")).toHaveLength( + 2, + ); }); it("requires exact database id confirmation before removal", async () => { const { cwd, stateDir } = await setupLinkedProject(); const wrongConfirm = await executeCli({ - argv: ["database", "remove", "acme-preview", "--confirm", "acme-preview", "--json"], + argv: [ + "database", + "remove", + "acme-preview", + "--confirm", + "acme-preview", + "--json", + ], cwd, stateDir, fixturePath, @@ -400,7 +461,14 @@ describe("database commands", () => { }); const exactConfirm = await executeCli({ - argv: ["database", "remove", "acme-preview", "--confirm", "db_123", "--json"], + argv: [ + "database", + "remove", + "acme-preview", + "--confirm", + "db_123", + "--json", + ], cwd, stateDir, fixturePath, @@ -424,7 +492,15 @@ describe("database commands", () => { const { cwd, stateDir } = await setupLinkedProject(); const wrongConfirm = await executeCli({ - argv: ["database", "connection", "remove", "conn_123", "--confirm", "primary", "--json"], + argv: [ + "database", + "connection", + "remove", + "conn_123", + "--confirm", + "primary", + "--json", + ], cwd, stateDir, fixturePath, @@ -446,7 +522,15 @@ describe("database commands", () => { }); const exactConfirm = await executeCli({ - argv: ["database", "connection", "remove", "conn_123", "--confirm", "conn_123", "--json"], + argv: [ + "database", + "connection", + "remove", + "conn_123", + "--confirm", + "conn_123", + "--json", + ], cwd, stateDir, fixturePath, diff --git a/packages/cli/tests/git-adapter.test.ts b/packages/cli/tests/git-adapter.test.ts index 254103d..88396ce 100644 --- a/packages/cli/tests/git-adapter.test.ts +++ b/packages/cli/tests/git-adapter.test.ts @@ -1,31 +1,42 @@ import { describe, expect, it } from "vitest"; -import { parseGitHubRepositoryUrl, readGitOriginRemote } from "../src/adapters/git"; +import { + parseGitHubRepositoryUrl, + readGitOriginRemote, +} from "../src/adapters/git"; describe("git adapter", () => { it("parses supported GitHub repository URLs", () => { - expect(parseGitHubRepositoryUrl("https://github.com/prisma/prisma-cli")).toEqual({ + expect( + parseGitHubRepositoryUrl("https://github.com/prisma/prisma-cli"), + ).toEqual({ provider: "github", owner: "prisma", name: "prisma-cli", fullName: "prisma/prisma-cli", url: "https://github.com/prisma/prisma-cli", }); - expect(parseGitHubRepositoryUrl("https://github.com/prisma/prisma-cli.git")).toEqual({ + expect( + parseGitHubRepositoryUrl("https://github.com/prisma/prisma-cli.git"), + ).toEqual({ provider: "github", owner: "prisma", name: "prisma-cli", fullName: "prisma/prisma-cli", url: "https://github.com/prisma/prisma-cli", }); - expect(parseGitHubRepositoryUrl("git@github.com:prisma/prisma-cli.git")).toEqual({ + expect( + parseGitHubRepositoryUrl("git@github.com:prisma/prisma-cli.git"), + ).toEqual({ provider: "github", owner: "prisma", name: "prisma-cli", fullName: "prisma/prisma-cli", url: "https://github.com/prisma/prisma-cli", }); - expect(parseGitHubRepositoryUrl("ssh://git@github.com/prisma/prisma-cli.git")).toEqual({ + expect( + parseGitHubRepositoryUrl("ssh://git@github.com/prisma/prisma-cli.git"), + ).toEqual({ provider: "github", owner: "prisma", name: "prisma-cli", @@ -35,8 +46,12 @@ describe("git adapter", () => { }); it("rejects unsupported providers and non-repository GitHub URLs", () => { - expect(parseGitHubRepositoryUrl("https://gitlab.com/prisma/prisma-cli")).toBeNull(); - expect(parseGitHubRepositoryUrl("https://github.com/prisma/prisma-cli/issues")).toBeNull(); + expect( + parseGitHubRepositoryUrl("https://gitlab.com/prisma/prisma-cli"), + ).toBeNull(); + expect( + parseGitHubRepositoryUrl("https://github.com/prisma/prisma-cli/issues"), + ).toBeNull(); expect(parseGitHubRepositoryUrl("not a url")).toBeNull(); }); @@ -44,7 +59,9 @@ describe("git adapter", () => { const controller = new AbortController(); controller.abort(); - await expect(readGitOriginRemote(process.cwd(), controller.signal)).rejects.toMatchObject({ + await expect( + readGitOriginRemote(process.cwd(), controller.signal), + ).rejects.toMatchObject({ name: "AbortError", }); }); diff --git a/packages/cli/tests/helpers.ts b/packages/cli/tests/helpers.ts index 2da9db3..85ec21c 100644 --- a/packages/cli/tests/helpers.ts +++ b/packages/cli/tests/helpers.ts @@ -5,7 +5,12 @@ import { PassThrough, Writable } from "node:stream"; import { runCli } from "../src/cli"; import { LocalStateStore } from "../src/adapters/local-state"; -import { createCommandContext, resolveStateDir, type CliRuntime, type CommandContext } from "../src/shell/runtime"; +import { + createCommandContext, + resolveStateDir, + type CliRuntime, + type CommandContext, +} from "../src/shell/runtime"; import type { GlobalFlags } from "../src/shell/global-flags"; class CaptureStream extends Writable { @@ -14,7 +19,11 @@ class CaptureStream extends Writable { declare columns?: number; declare rows?: number; - _write(chunk: Buffer | string, _encoding: BufferEncoding, callback: (error?: Error | null) => void) { + _write( + chunk: Buffer | string, + _encoding: BufferEncoding, + callback: (error?: Error | null) => void, + ) { this.buffer += chunk.toString(); callback(); } @@ -135,7 +144,10 @@ export async function createTestCommandContext(options: { }; return { - context: await seedRememberedProjectForTest(await createCommandContext(runtime, flags), runtime.env), + context: await seedRememberedProjectForTest( + await createCommandContext(runtime, flags), + runtime.env, + ), runtime, stdout, stderr, @@ -159,7 +171,9 @@ async function seedRememberedProjectForTest( return context; } -async function seedRememberedProjectStateForTest(runtime: CliRuntime): Promise { +async function seedRememberedProjectStateForTest( + runtime: CliRuntime, +): Promise { const projectId = runtime.env.PRISMA_CLI_TEST_REMEMBER_PROJECT_ID; if (!projectId) { return; @@ -172,7 +186,10 @@ async function seedRememberedProjectStateForTest(runtime: CliRuntime): Promise { +async function streamInput( + input: CaptureInput, + text: string | undefined, +): Promise { if (!text) { input.end(); return; diff --git a/packages/cli/tests/helpers/mock-factories.ts b/packages/cli/tests/helpers/mock-factories.ts index a49ac9c..62f5263 100644 --- a/packages/cli/tests/helpers/mock-factories.ts +++ b/packages/cli/tests/helpers/mock-factories.ts @@ -16,55 +16,81 @@ export function createProjectClient( return { token: "token", - GET: vi.fn().mockImplementation((pathName: string, request?: { params?: { query?: { gitName?: string } } }) => { - if (pathName === "/v1/projects") { - return { - data: { - data: [ - { - id: projectId, - name: projectId === "proj_456" ? "Billing API" : "Acme Dashboard", - slug: projectId === "proj_456" ? "billing-api" : "acme-dashboard", - workspace: { - id: "ws_123", - name: "Acme Inc", - }, + GET: vi + .fn() + .mockImplementation( + ( + pathName: string, + request?: { params?: { query?: { gitName?: string } } }, + ) => { + if (pathName === "/v1/projects") { + return { + data: { + data: [ + { + id: projectId, + name: + projectId === "proj_456" + ? "Billing API" + : "Acme Dashboard", + slug: + projectId === "proj_456" + ? "billing-api" + : "acme-dashboard", + workspace: { + id: "ws_123", + name: "Acme Inc", + }, + }, + ], }, - ], - }, - }; - } + }; + } - if (pathName === "/v1/projects/{projectId}/branches") { - const branchName = request?.params?.query?.gitName ?? "main"; - return { - data: { - data: options.branchExists === false ? [] : [branchRecord(branchName)], - }, - }; - } + if (pathName === "/v1/projects/{projectId}/branches") { + const branchName = request?.params?.query?.gitName ?? "main"; + return { + data: { + data: + options.branchExists === false + ? [] + : [branchRecord(branchName)], + }, + }; + } - throw new Error(`Unexpected path ${pathName}`); - }), - POST: vi.fn().mockImplementation((pathName: string, request?: { body?: { gitName?: string } }) => { - if (pathName === "/v1/projects/{projectId}/branches") { - const branchName = request?.body?.gitName ?? "main"; - return { - data: { - data: branchRecord(branchName), - }, - }; - } + throw new Error(`Unexpected path ${pathName}`); + }, + ), + POST: vi + .fn() + .mockImplementation( + (pathName: string, request?: { body?: { gitName?: string } }) => { + if (pathName === "/v1/projects/{projectId}/branches") { + const branchName = request?.body?.gitName ?? "main"; + return { + data: { + data: branchRecord(branchName), + }, + }; + } - throw new Error(`Unexpected path ${pathName}`); - }), + throw new Error(`Unexpected path ${pathName}`); + }, + ), }; } -export function createResolveBranch(role: "preview" | "production" = "preview") { - return vi.fn().mockImplementation((_projectId: string, options: { branchName: string }) => Promise.resolve({ - id: `branch_${options.branchName.replace(/[^a-z0-9]+/gi, "_")}`, - name: options.branchName, - role, - })); +export function createResolveBranch( + role: "preview" | "production" = "preview", +) { + return vi + .fn() + .mockImplementation((_projectId: string, options: { branchName: string }) => + Promise.resolve({ + id: `branch_${options.branchName.replace(/[^a-z0-9]+/gi, "_")}`, + name: options.branchName, + role, + }), + ); } diff --git a/packages/cli/tests/output.test.ts b/packages/cli/tests/output.test.ts index 25f18ab..34644cf 100644 --- a/packages/cli/tests/output.test.ts +++ b/packages/cli/tests/output.test.ts @@ -22,14 +22,18 @@ describe("shell output", () => { { label: "Uploaded" }, ]); - expect(plain(lines.join("\n"))).toBe([ - " Workspace Edith", - " Project j1 · created from package.json", - " Runtime HTTP 3000 · Next.js default", - " Uploaded", - ].join("\n")); + expect(plain(lines.join("\n"))).toBe( + [ + " Workspace Edith", + " Project j1 · created from package.json", + " Runtime HTTP 3000 · Next.js default", + " Uploaded", + ].join("\n"), + ); expect(lines[1]).toContain("\u001B[1m"); - expect(lines[1]).toContain("\u001B[2m· created from package.json\u001B[22m"); + expect(lines[1]).toContain( + "\u001B[2m· created from package.json\u001B[22m", + ); }); it("falls back to standard error formatting when humanLines is empty", async () => { @@ -47,12 +51,7 @@ describe("shell output", () => { expect(error.humanLines).toBeNull(); - writeHumanError( - context.output, - context.ui, - error, - { trace: false }, - ); + writeHumanError(context.output, context.ui, error, { trace: false }); expect(stderr.buffer).toContain("App deploy failed [DEPLOY_FAILED]"); expect(stderr.buffer).toContain("Why: Upload failed"); @@ -73,12 +72,7 @@ describe("shell output", () => { }); humanLines.push("Mutated after construction."); - writeHumanError( - context.output, - context.ui, - error, - { trace: false }, - ); + writeHumanError(context.output, context.ui, error, { trace: false }); expect(stderr.buffer).toContain("Custom failure."); expect(stderr.buffer).not.toContain("Mutated after construction."); @@ -100,7 +94,8 @@ describe("shell output", () => { summary: "App deploy failed", why: "ENOENT: missing file", fix: "Retry the command.", - debug: "Error: ENOENT: missing file\n at stageNextjsStandaloneArtifact", + debug: + "Error: ENOENT: missing file\n at stageNextjsStandaloneArtifact", }), { trace: true }, ); diff --git a/packages/cli/tests/production-deploy-gate.test.ts b/packages/cli/tests/production-deploy-gate.test.ts index e90c46e..ba8e1ba 100644 --- a/packages/cli/tests/production-deploy-gate.test.ts +++ b/packages/cli/tests/production-deploy-gate.test.ts @@ -3,11 +3,17 @@ import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { confirmPrompt } from "../src/shell/prompt"; -import type { PreviewAppProvider, PreviewAppRecord, PreviewDeploymentRecord } from "../src/lib/app/preview-provider"; +import type { + PreviewAppProvider, + PreviewAppRecord, + PreviewDeploymentRecord, +} from "../src/lib/app/preview-provider"; import { createTempCwd, createTestCommandContext } from "./helpers"; vi.mock("../src/shell/prompt", async () => { - const actual = await vi.importActual("../src/shell/prompt"); + const actual = await vi.importActual( + "../src/shell/prompt", + ); return { ...actual, confirmPrompt: vi.fn(), @@ -25,7 +31,9 @@ describe("production deploy gate", () => { const { context, stderr } = await createGateContext(); const provider = createGateProvider(); - const { enforceProductionDeployGate } = await import("../src/lib/app/production-deploy-gate"); + const { enforceProductionDeployGate } = await import( + "../src/lib/app/production-deploy-gate" + ); await enforceProductionDeployGate(context, provider, { appId: "app_1", appName: "hello-world", @@ -42,7 +50,9 @@ describe("production deploy gate", () => { const app = createApp({ liveDeploymentId: null }); const provider = createGateProvider(app, []); - const { enforceProductionDeployGate } = await import("../src/lib/app/production-deploy-gate"); + const { enforceProductionDeployGate } = await import( + "../src/lib/app/production-deploy-gate" + ); await enforceProductionDeployGate(context, provider, { appId: app.id, appName: app.name, @@ -51,20 +61,26 @@ describe("production deploy gate", () => { }); expect(provider.listDeployments).toHaveBeenCalledWith("app_1"); - expect(stderr.buffer).toContain('First deploy of "hello-world" -- promoting to production.'); + expect(stderr.buffer).toContain( + 'First deploy of "hello-world" -- promoting to production.', + ); }); it("blocks a subsequent production deploy without --prod", async () => { const { context } = await createGateContext(); const provider = createGateProvider(createApp(), [createDeployment()]); - const { enforceProductionDeployGate } = await import("../src/lib/app/production-deploy-gate"); - await expect(enforceProductionDeployGate(context, provider, { - appId: "app_1", - appName: "hello-world", - branchKind: "production", - prod: false, - })).rejects.toMatchObject({ + const { enforceProductionDeployGate } = await import( + "../src/lib/app/production-deploy-gate" + ); + await expect( + enforceProductionDeployGate(context, provider, { + appId: "app_1", + appName: "hello-world", + branchKind: "production", + prod: false, + }), + ).rejects.toMatchObject({ code: "PROD_DEPLOY_REQUIRES_FLAG", exitCode: 2, humanLines: [ @@ -87,7 +103,9 @@ describe("production deploy gate", () => { const { context, stderr } = await createGateContext({ isTTY: true }); const provider = createGateProvider(createApp(), [createDeployment()]); - const { enforceProductionDeployGate } = await import("../src/lib/app/production-deploy-gate"); + const { enforceProductionDeployGate } = await import( + "../src/lib/app/production-deploy-gate" + ); await enforceProductionDeployGate(context, provider, { appId: "app_1", appName: "hello-world", @@ -95,19 +113,27 @@ describe("production deploy gate", () => { prod: true, }); - expect(stderr.buffer).toContain("This will deploy to production and replace the live deployment."); + expect(stderr.buffer).toContain( + "This will deploy to production and replace the live deployment.", + ); expect(stderr.buffer).toContain("Current live: dep_live deployed"); - expect(mockedConfirmPrompt).toHaveBeenCalledWith(expect.objectContaining({ - message: "Deploy to production?", - initialValue: false, - })); + expect(mockedConfirmPrompt).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Deploy to production?", + initialValue: false, + }), + ); }); it("allows --prod --yes without prompting", async () => { - const { context, stderr } = await createGateContext({ flags: { yes: true } }); + const { context, stderr } = await createGateContext({ + flags: { yes: true }, + }); const provider = createGateProvider(createApp(), [createDeployment()]); - const { enforceProductionDeployGate } = await import("../src/lib/app/production-deploy-gate"); + const { enforceProductionDeployGate } = await import( + "../src/lib/app/production-deploy-gate" + ); await enforceProductionDeployGate(context, provider, { appId: "app_1", appName: "hello-world", @@ -123,13 +149,17 @@ describe("production deploy gate", () => { const { context } = await createGateContext({ flags: { yes: true } }); const provider = createGateProvider(createApp(), [createDeployment()]); - const { enforceProductionDeployGate } = await import("../src/lib/app/production-deploy-gate"); - await expect(enforceProductionDeployGate(context, provider, { - appId: "app_1", - appName: "hello-world", - branchKind: "production", - prod: false, - })).rejects.toMatchObject({ + const { enforceProductionDeployGate } = await import( + "../src/lib/app/production-deploy-gate" + ); + await expect( + enforceProductionDeployGate(context, provider, { + appId: "app_1", + appName: "hello-world", + branchKind: "production", + prod: false, + }), + ).rejects.toMatchObject({ code: "PROD_DEPLOY_REQUIRES_FLAG", exitCode: 2, }); @@ -137,8 +167,10 @@ describe("production deploy gate", () => { }); }); -async function createGateContext(options: Parameters[0] = {}) { - const cwd = options.cwd ?? await createTempCwd(); +async function createGateContext( + options: Parameters[0] = {}, +) { + const cwd = options.cwd ?? (await createTempCwd()); return createTestCommandContext({ ...options, cwd, @@ -158,7 +190,9 @@ function createGateProvider( }; } -function createApp(overrides: Partial = {}): PreviewAppRecord { +function createApp( + overrides: Partial = {}, +): PreviewAppRecord { return { id: "app_1", name: "hello-world", @@ -170,7 +204,9 @@ function createApp(overrides: Partial = {}): PreviewAppRecord }; } -function createDeployment(overrides: Partial = {}): PreviewDeploymentRecord { +function createDeployment( + overrides: Partial = {}, +): PreviewDeploymentRecord { return { id: "dep_live", status: "running", diff --git a/packages/cli/tests/project-controller.test.ts b/packages/cli/tests/project-controller.test.ts index e2a2117..044fce9 100644 --- a/packages/cli/tests/project-controller.test.ts +++ b/packages/cli/tests/project-controller.test.ts @@ -48,12 +48,14 @@ describe("project controller", () => { "prisma-cli project show --project ", ], }); - expect(result.nextActions).toEqual(expect.arrayContaining([ - expect.objectContaining({ - kind: "user-choice", - journey: "project-setup", - }), - ])); + expect(result.nextActions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + kind: "user-choice", + journey: "project-setup", + }), + ]), + ); }); it("links an existing project and writes the local pin", async () => { @@ -86,8 +88,12 @@ describe("project controller", () => { }, action: "linked", }); - await expect(readFile(path.join(cwd, ".prisma/local.json"), "utf8")).resolves.toContain('"projectId": "proj_123"'); - await expect(readFile(path.join(cwd, ".gitignore"), "utf8")).resolves.toBe(".prisma/\n"); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).resolves.toContain('"projectId": "proj_123"'); + await expect(readFile(path.join(cwd, ".gitignore"), "utf8")).resolves.toBe( + ".prisma/\n", + ); }); it("returns LOCAL_STATE_WRITE_FAILED when the local pin cannot be written", async () => { @@ -144,7 +150,9 @@ describe("project controller", () => { operation: "read", }, }); - await expect(readFile(path.join(cwd, ".prisma/local.json"), "utf8")).resolves.toContain('"projectId": "proj_123"'); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).resolves.toContain('"projectId": "proj_123"'); }); it("creates a project and writes the local pin", async () => { @@ -208,8 +216,12 @@ describe("project controller", () => { }, action: "created", }); - await expect(readFile(path.join(cwd, ".prisma/local.json"), "utf8")).resolves.toContain('"projectId": "proj_new"'); - await expect(readFile(path.join(cwd, ".gitignore"), "utf8")).resolves.toBe(".prisma/\n"); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).resolves.toContain('"projectId": "proj_new"'); + await expect(readFile(path.join(cwd, ".gitignore"), "utf8")).resolves.toBe( + ".prisma/\n", + ); }); it("bare project link can create a new project from the interactive setup picker", async () => { @@ -261,7 +273,11 @@ describe("project controller", () => { })); const cwd = await createTempCwd(); - await writeFile(path.join(cwd, "package.json"), `${JSON.stringify({ name: "suggested-name" }, null, 2)}\n`, "utf8"); + await writeFile( + path.join(cwd, "package.json"), + `${JSON.stringify({ name: "suggested-name" }, null, 2)}\n`, + "utf8", + ); const stateDir = path.join(cwd, ".state"); const { context, stderr } = await createTestCommandContext({ cwd, @@ -293,7 +309,9 @@ describe("project controller", () => { }, action: "created", }); - await expect(readFile(path.join(cwd, ".prisma/local.json"), "utf8")).resolves.toContain('"projectId": "proj_new"'); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).resolves.toContain('"projectId": "proj_new"'); expect(stderr.buffer).toContain("Which Project should this directory use?"); expect(stderr.buffer).toContain("Project name"); expect(stderr.buffer).toContain("suggested-name"); @@ -301,7 +319,9 @@ describe("project controller", () => { it("returns PROJECT_CREATE_FAILED when project creation fails", async () => { const requireComputeAuth = vi.fn().mockResolvedValue({ token: "token" }); - const createProject = vi.fn().mockRejectedValue(new Error("Internal Server Error (HTTP 503)")); + const createProject = vi + .fn() + .mockRejectedValue(new Error("Internal Server Error (HTTP 503)")); vi.doMock("../src/lib/auth/auth-ops", () => ({ readAuthState: vi.fn().mockResolvedValue({ @@ -340,12 +360,16 @@ describe("project controller", () => { }); const { runProjectCreate } = await import("../src/controllers/project"); - await expect(runProjectCreate(context, "New Dashboard")).rejects.toMatchObject({ + await expect( + runProjectCreate(context, "New Dashboard"), + ).rejects.toMatchObject({ code: "PROJECT_CREATE_FAILED", domain: "project", summary: 'Could not create Project "New Dashboard"', why: expect.stringContaining("Internal Server Error"), - nextSteps: expect.arrayContaining(["prisma-cli project link "]), + nextSteps: expect.arrayContaining([ + "prisma-cli project link ", + ]), }); }); }); diff --git a/packages/cli/tests/project-real-mode.test.ts b/packages/cli/tests/project-real-mode.test.ts index e2c3205..1585394 100644 --- a/packages/cli/tests/project-real-mode.test.ts +++ b/packages/cli/tests/project-real-mode.test.ts @@ -24,27 +24,48 @@ function mockAuthState() { }); } -function mockClient(extra: Partial<{ - GET: ReturnType; - POST: ReturnType; - DELETE: ReturnType; -}> = {}) { +function mockClient( + extra: Partial<{ + GET: ReturnType; + POST: ReturnType; + DELETE: ReturnType; + }> = {}, +) { return { - GET: extra.GET ?? vi.fn().mockImplementation((pathName: string) => { - if (pathName === "/v1/projects") { - return { - data: { - data: [ - { id: "proj_456", name: "Billing API", slug: "billing-api", url: "https://prisma.build/acme/billing-api", workspace: { id: "ws_123", name: "Acme Inc" } }, - { id: "proj_999", name: "Alpha", slug: "alpha", workspace: { id: "ws_other", name: "Other" } }, - { id: "proj_123", name: "Acme Dashboard", slug: "acme-dashboard", url: "https://prisma.build/acme/acme-dashboard", workspace: { id: "ws_123", name: "Acme Inc" } }, - ], - }, - }; - } + GET: + extra.GET ?? + vi.fn().mockImplementation((pathName: string) => { + if (pathName === "/v1/projects") { + return { + data: { + data: [ + { + id: "proj_456", + name: "Billing API", + slug: "billing-api", + url: "https://prisma.build/acme/billing-api", + workspace: { id: "ws_123", name: "Acme Inc" }, + }, + { + id: "proj_999", + name: "Alpha", + slug: "alpha", + workspace: { id: "ws_other", name: "Other" }, + }, + { + id: "proj_123", + name: "Acme Dashboard", + slug: "acme-dashboard", + url: "https://prisma.build/acme/acme-dashboard", + workspace: { id: "ws_123", name: "Acme Inc" }, + }, + ], + }, + }; + } - throw new Error(`Unexpected path ${pathName}`); - }), + throw new Error(`Unexpected path ${pathName}`); + }), POST: extra.POST ?? vi.fn(), DELETE: extra.DELETE ?? vi.fn(), }; @@ -121,7 +142,9 @@ describe("real project mode", () => { requireComputeAuth, })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runProjectList } = await import("../src/controllers/project"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -136,27 +159,43 @@ describe("real project mode", () => { const result = await runProjectList(context); - expect(readAuthState).toHaveBeenCalledWith(context.runtime.env, context.runtime.signal); - expect(requireComputeAuth).toHaveBeenCalledWith(context.runtime.env, context.runtime.signal); + expect(readAuthState).toHaveBeenCalledWith( + context.runtime.env, + context.runtime.signal, + ); + expect(requireComputeAuth).toHaveBeenCalledWith( + context.runtime.env, + context.runtime.signal, + ); expect(result.result).toEqual({ workspace: { id: "ws_123", name: "Acme Inc", }, projects: [ - { id: "proj_123", name: "Acme Dashboard", url: "https://prisma.build/acme/acme-dashboard" }, - { id: "proj_456", name: "Billing API", url: "https://prisma.build/acme/billing-api" }, + { + id: "proj_123", + name: "Acme Dashboard", + url: "https://prisma.build/acme/acme-dashboard", + }, + { + id: "proj_456", + name: "Billing API", + url: "https://prisma.build/acme/billing-api", + }, ], localBinding: { status: "not-linked", }, }); - expect(result.nextActions).toEqual(expect.arrayContaining([ - expect.objectContaining({ - kind: "user-choice", - journey: "project-setup", - }), - ])); + expect(result.nextActions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + kind: "user-choice", + journey: "project-setup", + }), + ]), + ); }); it("resolves an explicit project in real mode", async () => { @@ -169,7 +208,9 @@ describe("real project mode", () => { requireComputeAuth: vi.fn().mockResolvedValue(mockClient()), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runProjectShow } = await import("../src/controllers/project"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -201,116 +242,127 @@ describe("real project mode", () => { }); it("connects a GitHub repository through an installed GitHub App", async () => { - const get = vi.fn().mockImplementation((pathName: string, request?: { params?: { query?: Record } }) => { - if (pathName === "/v1/projects") { - return mockClient().GET(pathName); - } - - if (pathName === "/v1/source-repositories") { - return sourceRepositoryList(); - } + const get = vi + .fn() + .mockImplementation( + ( + pathName: string, + request?: { params?: { query?: Record } }, + ) => { + if (pathName === "/v1/projects") { + return mockClient().GET(pathName); + } + + if (pathName === "/v1/source-repositories") { + return sourceRepositoryList(); + } + + if (pathName === "/v1/scm-installations") { + expect(request?.params?.query).toEqual({ + workspaceId: "ws_123", + limit: 100, + }); + return { + data: { + data: [ + { + id: "scminstall_123", + type: "scm-installation", + url: "https://api.prisma.test/v1/scm-installations/scminstall_123", + provider: "github", + installationId: 98765, + accountId: 111, + accountLogin: "prisma", + accountType: "organization", + suspended: false, + createdAt: "2026-05-18T00:00:00.000Z", + updatedAt: "2026-05-18T00:00:00.000Z", + }, + ], + pagination: { + nextCursor: null, + hasMore: false, + }, + }, + }; + } + + if ( + pathName === "/v1/scm-installations/{installationId}/repositories" + ) { + if (request?.params?.query?.cursor === "2") { + return { + data: { + data: [ + { + id: 123456, + type: "scm-repository", + fullName: "prisma/prisma-cli", + defaultBranch: "main", + isPrivate: true, + }, + ], + pagination: { + nextCursor: null, + hasMore: false, + }, + }, + }; + } - if (pathName === "/v1/scm-installations") { - expect(request?.params?.query).toEqual({ - workspaceId: "ws_123", - limit: 100, - }); - return { - data: { - data: [ - { - id: "scminstall_123", - type: "scm-installation", - url: "https://api.prisma.test/v1/scm-installations/scminstall_123", - provider: "github", - installationId: 98765, - accountId: 111, - accountLogin: "prisma", - accountType: "organization", - suspended: false, - createdAt: "2026-05-18T00:00:00.000Z", - updatedAt: "2026-05-18T00:00:00.000Z", + return { + data: { + data: [ + { + id: 999, + type: "scm-repository", + fullName: "prisma/other", + defaultBranch: "main", + isPrivate: false, + }, + ], + pagination: { + nextCursor: "2", + hasMore: true, + }, }, - ], - pagination: { - nextCursor: null, - hasMore: false, - }, - }, - }; - } + }; + } - if (pathName === "/v1/scm-installations/{installationId}/repositories") { - if (request?.params?.query?.cursor === "2") { + throw new Error(`Unexpected path ${pathName}`); + }, + ); + const post = vi + .fn() + .mockImplementation((pathName: string, request?: { body?: unknown }) => { + if (pathName === "/v1/source-repositories") { + expect(request?.body).toEqual({ + projectId: "proj_123", + provider: "github", + providerRepositoryId: 123456, + installationId: "scminstall_123", + }); return { data: { - data: [ - { - id: 123456, - type: "scm-repository", - fullName: "prisma/prisma-cli", - defaultBranch: "main", - isPrivate: true, - }, - ], - pagination: { - nextCursor: null, - hasMore: false, + data: { + id: "srcrepo_123", + repoId: 123456, + provider: "github", + repoFullName: "prisma/prisma-cli", + defaultBranch: "main", + isPrivate: true, + status: "active", + projectId: "proj_123", + installationId: "scminstall_123", + createdAt: "2026-05-18T00:00:00.000Z", + updatedAt: "2026-05-18T00:00:00.000Z", }, }, }; } - return { - data: { - data: [ - { - id: 999, - type: "scm-repository", - fullName: "prisma/other", - defaultBranch: "main", - isPrivate: false, - }, - ], - pagination: { - nextCursor: "2", - hasMore: true, - }, - }, - }; - } - - throw new Error(`Unexpected path ${pathName}`); - }); - const post = vi.fn().mockImplementation((pathName: string, request?: { body?: unknown }) => { - if (pathName === "/v1/source-repositories") { - expect(request?.body).toEqual({ - projectId: "proj_123", - provider: "github", - providerRepositoryId: 123456, - installationId: "scminstall_123", - }); - return { - data: { - data: { - id: "srcrepo_123", - repoId: 123456, - provider: "github", - repoFullName: "prisma/prisma-cli", - defaultBranch: "main", - isPrivate: true, - status: "active", - projectId: "proj_123", - installationId: "scminstall_123", - createdAt: "2026-05-18T00:00:00.000Z", - updatedAt: "2026-05-18T00:00:00.000Z", - }, - }, - }; - } - - throw new Error(`Unexpected path ${pathName}`); - }); + throw new Error(`Unexpected path ${pathName}`); + }); vi.doMock("../src/lib/auth/auth-ops", () => ({ readAuthState: mockAuthState(), @@ -318,10 +370,14 @@ describe("real project mode", () => { performLogout: vi.fn(), })); vi.doMock("../src/lib/auth/guard", () => ({ - requireComputeAuth: vi.fn().mockResolvedValue(mockClient({ GET: get, POST: post })), + requireComputeAuth: vi + .fn() + .mockResolvedValue(mockClient({ GET: get, POST: post })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runGitConnect } = await import("../src/controllers/project"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -334,21 +390,28 @@ describe("real project mode", () => { }, }); - const result = await runGitConnect(context, "https://github.com/prisma/prisma-cli", { project: "proj_123" }); + const result = await runGitConnect( + context, + "https://github.com/prisma/prisma-cli", + { project: "proj_123" }, + ); expect(post).toHaveBeenCalledOnce(); - expect(get).toHaveBeenCalledWith("/v1/scm-installations/{installationId}/repositories", { - params: { - path: { - installationId: "scminstall_123", - }, - query: { - limit: 100, - cursor: "2", + expect(get).toHaveBeenCalledWith( + "/v1/scm-installations/{installationId}/repositories", + { + params: { + path: { + installationId: "scminstall_123", + }, + query: { + limit: 100, + cursor: "2", + }, }, + signal: context.runtime.signal, }, - signal: context.runtime.signal, - }); + ); expect(result.result.repositoryConnection).toMatchObject({ id: "srcrepo_123", repoId: 123456, @@ -389,10 +452,14 @@ describe("real project mode", () => { performLogout: vi.fn(), })); vi.doMock("../src/lib/auth/guard", () => ({ - requireComputeAuth: vi.fn().mockResolvedValue(mockClient({ GET: get, POST: post })), + requireComputeAuth: vi + .fn() + .mockResolvedValue(mockClient({ GET: get, POST: post })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runGitConnect } = await import("../src/controllers/project"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -405,7 +472,11 @@ describe("real project mode", () => { }, }); - const result = await runGitConnect(context, "https://github.com/prisma/prisma-cli", { project: "proj_123" }); + const result = await runGitConnect( + context, + "https://github.com/prisma/prisma-cli", + { project: "proj_123" }, + ); expect(post).not.toHaveBeenCalled(); expect(result.result.repositoryConnection).toMatchObject({ @@ -441,26 +512,29 @@ describe("real project mode", () => { throw new Error(`Unexpected path ${pathName}`); }); - const post = vi.fn().mockImplementation((pathName: string, request?: { body?: unknown }) => { - if (pathName === "/v1/scm-installations/install-intents") { - expect(request?.body).toEqual({ - provider: "github", - workspaceId: "ws_123", - }); - return { - data: { + const post = vi + .fn() + .mockImplementation((pathName: string, request?: { body?: unknown }) => { + if (pathName === "/v1/scm-installations/install-intents") { + expect(request?.body).toEqual({ + provider: "github", + workspaceId: "ws_123", + }); + return { data: { - type: "install-intent", - provider: "github", - workspaceId: "wksp_123", - installUrl: "https://github.com/apps/prisma/installations/new?state=abc", + data: { + type: "install-intent", + provider: "github", + workspaceId: "wksp_123", + installUrl: + "https://github.com/apps/prisma/installations/new?state=abc", + }, }, - }, - }; - } + }; + } - throw new Error(`Unexpected path ${pathName}`); - }); + throw new Error(`Unexpected path ${pathName}`); + }); vi.doMock("../src/lib/auth/auth-ops", () => ({ readAuthState: mockAuthState(), @@ -468,10 +542,14 @@ describe("real project mode", () => { performLogout: vi.fn(), })); vi.doMock("../src/lib/auth/guard", () => ({ - requireComputeAuth: vi.fn().mockResolvedValue(mockClient({ GET: get, POST: post })), + requireComputeAuth: vi + .fn() + .mockResolvedValue(mockClient({ GET: get, POST: post })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runGitConnect } = await import("../src/controllers/project"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -484,16 +562,19 @@ describe("real project mode", () => { }, }); - await expect(runGitConnect(context, "https://github.com/prisma/prisma-cli", { project: "proj_123" })) - .rejects - .toMatchObject({ - code: "REPO_INSTALLATION_REQUIRED", - meta: { - installUrl: "https://github.com/apps/prisma/installations/new?state=abc", - opened: false, - repository: "prisma/prisma-cli", - }, - }); + await expect( + runGitConnect(context, "https://github.com/prisma/prisma-cli", { + project: "proj_123", + }), + ).rejects.toMatchObject({ + code: "REPO_INSTALLATION_REQUIRED", + meta: { + installUrl: + "https://github.com/apps/prisma/installations/new?state=abc", + opened: false, + repository: "prisma/prisma-cli", + }, + }); }); it("creates an install intent when the stored GitHub App installation is unavailable", async () => { @@ -547,26 +628,29 @@ describe("real project mode", () => { throw new Error(`Unexpected path ${pathName}`); }); - const post = vi.fn().mockImplementation((pathName: string, request?: { body?: unknown }) => { - if (pathName === "/v1/scm-installations/install-intents") { - expect(request?.body).toEqual({ - provider: "github", - workspaceId: "ws_123", - }); - return { - data: { + const post = vi + .fn() + .mockImplementation((pathName: string, request?: { body?: unknown }) => { + if (pathName === "/v1/scm-installations/install-intents") { + expect(request?.body).toEqual({ + provider: "github", + workspaceId: "ws_123", + }); + return { data: { - type: "install-intent", - provider: "github", - workspaceId: "wksp_123", - installUrl: "https://github.com/apps/prisma/installations/new?state=abc", + data: { + type: "install-intent", + provider: "github", + workspaceId: "wksp_123", + installUrl: + "https://github.com/apps/prisma/installations/new?state=abc", + }, }, - }, - }; - } + }; + } - throw new Error(`Unexpected path ${pathName}`); - }); + throw new Error(`Unexpected path ${pathName}`); + }); vi.doMock("../src/lib/auth/auth-ops", () => ({ readAuthState: mockAuthState(), @@ -574,10 +658,14 @@ describe("real project mode", () => { performLogout: vi.fn(), })); vi.doMock("../src/lib/auth/guard", () => ({ - requireComputeAuth: vi.fn().mockResolvedValue(mockClient({ GET: get, POST: post })), + requireComputeAuth: vi + .fn() + .mockResolvedValue(mockClient({ GET: get, POST: post })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runGitConnect } = await import("../src/controllers/project"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -590,16 +678,19 @@ describe("real project mode", () => { }, }); - await expect(runGitConnect(context, "https://github.com/prisma/prisma-cli", { project: "proj_123" })) - .rejects - .toMatchObject({ - code: "REPO_INSTALLATION_REQUIRED", - meta: { - installUrl: "https://github.com/apps/prisma/installations/new?state=abc", - opened: false, - repository: "prisma/prisma-cli", - }, - }); + await expect( + runGitConnect(context, "https://github.com/prisma/prisma-cli", { + project: "proj_123", + }), + ).rejects.toMatchObject({ + code: "REPO_INSTALLATION_REQUIRED", + meta: { + installUrl: + "https://github.com/apps/prisma/installations/new?state=abc", + opened: false, + repository: "prisma/prisma-cli", + }, + }); expect(post).toHaveBeenCalledOnce(); }); @@ -621,23 +712,24 @@ describe("real project mode", () => { installationListCalls += 1; return { data: { - data: installationListCalls === 1 - ? [] - : [ - { - id: "scminstall_123", - type: "scm-installation", - url: "https://api.prisma.test/v1/scm-installations/scminstall_123", - provider: "github", - installationId: 98765, - accountId: 111, - accountLogin: "prisma", - accountType: "organization", - suspended: false, - createdAt: "2026-05-18T00:00:00.000Z", - updatedAt: "2026-05-18T00:00:00.000Z", - }, - ], + data: + installationListCalls === 1 + ? [] + : [ + { + id: "scminstall_123", + type: "scm-installation", + url: "https://api.prisma.test/v1/scm-installations/scminstall_123", + provider: "github", + installationId: 98765, + accountId: 111, + accountLogin: "prisma", + accountType: "organization", + suspended: false, + createdAt: "2026-05-18T00:00:00.000Z", + updatedAt: "2026-05-18T00:00:00.000Z", + }, + ], pagination: { nextCursor: null, hasMore: false, @@ -668,52 +760,55 @@ describe("real project mode", () => { throw new Error(`Unexpected path ${pathName}`); }); - const post = vi.fn().mockImplementation((pathName: string, request?: { body?: unknown }) => { - if (pathName === "/v1/scm-installations/install-intents") { - expect(request?.body).toEqual({ - provider: "github", - workspaceId: "ws_123", - }); - return { - data: { + const post = vi + .fn() + .mockImplementation((pathName: string, request?: { body?: unknown }) => { + if (pathName === "/v1/scm-installations/install-intents") { + expect(request?.body).toEqual({ + provider: "github", + workspaceId: "ws_123", + }); + return { data: { - type: "install-intent", - provider: "github", - workspaceId: "wksp_123", - installUrl: "https://github.com/apps/prisma/installations/new?state=abc", + data: { + type: "install-intent", + provider: "github", + workspaceId: "wksp_123", + installUrl: + "https://github.com/apps/prisma/installations/new?state=abc", + }, }, - }, - }; - } + }; + } - if (pathName === "/v1/source-repositories") { - expect(request?.body).toEqual({ - projectId: "proj_123", - provider: "github", - providerRepositoryId: 123456, - installationId: "scminstall_123", - }); - return { - data: { + if (pathName === "/v1/source-repositories") { + expect(request?.body).toEqual({ + projectId: "proj_123", + provider: "github", + providerRepositoryId: 123456, + installationId: "scminstall_123", + }); + return { data: { - id: "srcrepo_123", - repoId: 123456, - provider: "github", - repoFullName: "prisma/prisma-cli", - defaultBranch: "main", - isPrivate: true, - status: "active", - projectId: "proj_123", - installationId: "scminstall_123", - createdAt: "2026-05-18T00:00:00.000Z", - updatedAt: "2026-05-18T00:00:00.000Z", + data: { + id: "srcrepo_123", + repoId: 123456, + provider: "github", + repoFullName: "prisma/prisma-cli", + defaultBranch: "main", + isPrivate: true, + status: "active", + projectId: "proj_123", + installationId: "scminstall_123", + createdAt: "2026-05-18T00:00:00.000Z", + updatedAt: "2026-05-18T00:00:00.000Z", + }, }, - }, - }; - } + }; + } - throw new Error(`Unexpected path ${pathName}`); - }); + throw new Error(`Unexpected path ${pathName}`); + }); vi.doMock("../src/lib/auth/auth-ops", () => ({ readAuthState: mockAuthState(), @@ -721,10 +816,14 @@ describe("real project mode", () => { performLogout: vi.fn(), })); vi.doMock("../src/lib/auth/guard", () => ({ - requireComputeAuth: vi.fn().mockResolvedValue(mockClient({ GET: get, POST: post })), + requireComputeAuth: vi + .fn() + .mockResolvedValue(mockClient({ GET: get, POST: post })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runGitConnect } = await import("../src/controllers/project"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -741,9 +840,15 @@ describe("real project mode", () => { }, }); - const result = await runGitConnect(context, "https://github.com/prisma/prisma-cli", { project: "proj_123" }); + const result = await runGitConnect( + context, + "https://github.com/prisma/prisma-cli", + { project: "proj_123" }, + ); - expect(openBrowser).toHaveBeenCalledWith("https://github.com/apps/prisma/installations/new?state=abc"); + expect(openBrowser).toHaveBeenCalledWith( + "https://github.com/apps/prisma/installations/new?state=abc", + ); expect(installationListCalls).toBe(2); expect(post).toHaveBeenCalledWith("/v1/source-repositories", { body: { @@ -754,8 +859,12 @@ describe("real project mode", () => { }, signal: context.runtime.signal, }); - expect(stderr.buffer).toContain("Waiting for GitHub App installation or repository access approval"); - expect(result.result.repositoryConnection?.repository.fullName).toBe("prisma/prisma-cli"); + expect(stderr.buffer).toContain( + "Waiting for GitHub App installation or repository access approval", + ); + expect(result.result.repositoryConnection?.repository.fullName).toBe( + "prisma/prisma-cli", + ); }); it("returns REPO_NOT_ACCESSIBLE when the GitHub App cannot see the repository", async () => { @@ -816,26 +925,29 @@ describe("real project mode", () => { throw new Error(`Unexpected path ${pathName}`); }); - const post = vi.fn().mockImplementation((pathName: string, request?: { body?: unknown }) => { - if (pathName === "/v1/scm-installations/install-intents") { - expect(request?.body).toEqual({ - provider: "github", - workspaceId: "ws_123", - }); - return { - data: { + const post = vi + .fn() + .mockImplementation((pathName: string, request?: { body?: unknown }) => { + if (pathName === "/v1/scm-installations/install-intents") { + expect(request?.body).toEqual({ + provider: "github", + workspaceId: "ws_123", + }); + return { data: { - type: "install-intent", - provider: "github", - workspaceId: "wksp_123", - installUrl: "https://github.com/apps/prisma/installations/new?state=abc", + data: { + type: "install-intent", + provider: "github", + workspaceId: "wksp_123", + installUrl: + "https://github.com/apps/prisma/installations/new?state=abc", + }, }, - }, - }; - } + }; + } - throw new Error(`Unexpected path ${pathName}`); - }); + throw new Error(`Unexpected path ${pathName}`); + }); vi.doMock("../src/lib/auth/auth-ops", () => ({ readAuthState: mockAuthState(), @@ -843,10 +955,14 @@ describe("real project mode", () => { performLogout: vi.fn(), })); vi.doMock("../src/lib/auth/guard", () => ({ - requireComputeAuth: vi.fn().mockResolvedValue(mockClient({ GET: get, POST: post })), + requireComputeAuth: vi + .fn() + .mockResolvedValue(mockClient({ GET: get, POST: post })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runGitConnect } = await import("../src/controllers/project"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -859,16 +975,19 @@ describe("real project mode", () => { }, }); - await expect(runGitConnect(context, "https://github.com/prisma/prisma-cli", { project: "proj_123" })) - .rejects - .toMatchObject({ - code: "REPO_NOT_ACCESSIBLE", - meta: { - installUrl: "https://github.com/apps/prisma/installations/new?state=abc", - opened: false, - repository: "prisma/prisma-cli", - }, - }); + await expect( + runGitConnect(context, "https://github.com/prisma/prisma-cli", { + project: "proj_123", + }), + ).rejects.toMatchObject({ + code: "REPO_NOT_ACCESSIBLE", + meta: { + installUrl: + "https://github.com/apps/prisma/installations/new?state=abc", + opened: false, + repository: "prisma/prisma-cli", + }, + }); expect(post).toHaveBeenCalledOnce(); }); @@ -904,10 +1023,14 @@ describe("real project mode", () => { performLogout: vi.fn(), })); vi.doMock("../src/lib/auth/guard", () => ({ - requireComputeAuth: vi.fn().mockResolvedValue(mockClient({ GET: get, POST: post })), + requireComputeAuth: vi + .fn() + .mockResolvedValue(mockClient({ GET: get, POST: post })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runGitConnect } = await import("../src/controllers/project"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -920,12 +1043,14 @@ describe("real project mode", () => { }, }); - await expect(runGitConnect(context, "https://github.com/prisma/prisma-cli", { project: "proj_123" })) - .rejects - .toMatchObject({ - code: "REPO_CONNECTION_FAILED", - why: "Pagination cursor did not advance.", - }); + await expect( + runGitConnect(context, "https://github.com/prisma/prisma-cli", { + project: "proj_123", + }), + ).rejects.toMatchObject({ + code: "REPO_CONNECTION_FAILED", + why: "Pagination cursor did not advance.", + }); expect(post).not.toHaveBeenCalled(); }); @@ -973,10 +1098,14 @@ describe("real project mode", () => { performLogout: vi.fn(), })); vi.doMock("../src/lib/auth/guard", () => ({ - requireComputeAuth: vi.fn().mockResolvedValue(mockClient({ GET: get, POST: post })), + requireComputeAuth: vi + .fn() + .mockResolvedValue(mockClient({ GET: get, POST: post })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runGitConnect } = await import("../src/controllers/project"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -989,12 +1118,14 @@ describe("real project mode", () => { }, }); - await expect(runGitConnect(context, "https://github.com/prisma/prisma-cli", { project: "proj_123" })) - .rejects - .toMatchObject({ - code: "REPO_CONNECTION_FAILED", - why: "Pagination cursor did not advance.", - }); + await expect( + runGitConnect(context, "https://github.com/prisma/prisma-cli", { + project: "proj_123", + }), + ).rejects.toMatchObject({ + code: "REPO_CONNECTION_FAILED", + why: "Pagination cursor did not advance.", + }); expect(post).not.toHaveBeenCalled(); }); @@ -1040,10 +1171,14 @@ describe("real project mode", () => { performLogout: vi.fn(), })); vi.doMock("../src/lib/auth/guard", () => ({ - requireComputeAuth: vi.fn().mockResolvedValue(mockClient({ GET: get, DELETE: del })), + requireComputeAuth: vi + .fn() + .mockResolvedValue(mockClient({ GET: get, DELETE: del })), })); - const { createTempCwd, createTestCommandContext } = await import("./helpers"); + const { createTempCwd, createTestCommandContext } = await import( + "./helpers" + ); const { runGitDisconnect } = await import("../src/controllers/project"); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); @@ -1066,6 +1201,8 @@ describe("real project mode", () => { }, signal: context.runtime.signal, }); - expect(result.result.repositoryConnection?.repository.fullName).toBe("prisma/prisma-cli"); + expect(result.result.repositoryConnection?.repository.fullName).toBe( + "prisma/prisma-cli", + ); }); }); diff --git a/packages/cli/tests/project-resolution.test.ts b/packages/cli/tests/project-resolution.test.ts index d969157..9e3bb35 100644 --- a/packages/cli/tests/project-resolution.test.ts +++ b/packages/cli/tests/project-resolution.test.ts @@ -4,12 +4,19 @@ import type { Result } from "better-result"; import { describe, expect, it, vi } from "vitest"; import { createTempCwd, createTestCommandContext } from "./helpers"; -import { projectResolutionErrorToCliError, resolveProjectTarget } from "../src/lib/project/resolution"; +import { + projectResolutionErrorToCliError, + resolveProjectTarget, +} from "../src/lib/project/resolution"; import type { ProjectCandidate } from "../src/lib/project/resolution"; async function writeLocalPin(cwd: string, pin: unknown) { await mkdir(path.join(cwd, ".prisma"), { recursive: true }); - await writeFile(path.join(cwd, ".prisma/local.json"), `${JSON.stringify(pin, null, 2)}\n`, "utf8"); + await writeFile( + path.join(cwd, ".prisma/local.json"), + `${JSON.stringify(pin, null, 2)}\n`, + "utf8", + ); } async function writeLocalPinContent(cwd: string, content: string) { @@ -25,7 +32,10 @@ function expectOk(result: Result): T { return result.value; } -function expectErr(result: Result, expectedTag: E["_tag"]): E { +function expectErr( + result: Result, + expectedTag: E["_tag"], +): E { expect(result.isErr()).toBe(true); if (!result.isErr()) { throw new Error("Expected Result to be Err"); @@ -76,14 +86,18 @@ describe("project resolution", () => { projectId: "proj_123", }); const { context } = await createTestCommandContext({ cwd }); - const listProjects = vi.fn(async (): Promise => [{ - id: "proj_active", - name: "Active Project", - workspace: { - id: "ws_123", - name: "Acme Inc", - }, - }]); + const listProjects = vi.fn( + async (): Promise => [ + { + id: "proj_active", + name: "Active Project", + workspace: { + id: "ws_123", + name: "Acme Inc", + }, + }, + ], + ); const result = await resolveProjectTarget({ context, @@ -109,14 +123,18 @@ describe("project resolution", () => { projectId: "proj_123", }); const { context } = await createTestCommandContext({ cwd }); - const listProjects = vi.fn(async (): Promise => [{ - id: "proj_env", - name: "Env Project", - workspace: { - id: "ws_123", - name: "Acme Inc", - }, - }]); + const listProjects = vi.fn( + async (): Promise => [ + { + id: "proj_env", + name: "Env Project", + workspace: { + id: "ws_123", + name: "Acme Inc", + }, + }, + ], + ); const result = await resolveProjectTarget({ context, diff --git a/packages/cli/tests/project.test.ts b/packages/cli/tests/project.test.ts index bf24652..270c7ba 100644 --- a/packages/cli/tests/project.test.ts +++ b/packages/cli/tests/project.test.ts @@ -23,7 +23,11 @@ async function login( } async function writePackageJson(cwd: string, name: string) { - await writeFile(path.join(cwd, "package.json"), `${JSON.stringify({ name }, null, 2)}\n`, "utf8"); + await writeFile( + path.join(cwd, "package.json"), + `${JSON.stringify({ name }, null, 2)}\n`, + "utf8", + ); } async function writeLocalPin(cwd: string, pin: unknown | string) { @@ -37,7 +41,13 @@ async function writeLocalPin(cwd: string, pin: unknown | string) { async function createAmbiguousFixture(cwd: string): Promise { const raw = JSON.parse(await readFile(fixturePath, "utf8")) as { - projects: Array<{ id: string; name: string; slug: string; url?: string; workspaceId: string }>; + projects: Array<{ + id: string; + name: string; + slug: string; + url?: string; + workspaceId: string; + }>; }; raw.projects.push({ id: "proj_321", @@ -53,7 +63,13 @@ async function createAmbiguousFixture(cwd: string): Promise { async function createAppleFixture(cwd: string): Promise { const raw = JSON.parse(await readFile(fixturePath, "utf8")) as { - projects: Array<{ id: string; name: string; slug: string; url?: string; workspaceId: string }>; + projects: Array<{ + id: string; + name: string; + slug: string; + url?: string; + workspaceId: string; + }>; }; raw.projects = [ { @@ -72,7 +88,13 @@ async function createAppleFixture(cwd: string): Promise { async function createEdithOrangeFixture(cwd: string): Promise { const raw = JSON.parse(await readFile(fixturePath, "utf8")) as { workspaces: Array<{ id: string; name: string; slug: string }>; - projects: Array<{ id: string; name: string; slug: string; url?: string; workspaceId: string }>; + projects: Array<{ + id: string; + name: string; + slug: string; + url?: string; + workspaceId: string; + }>; }; raw.workspaces = [ { @@ -135,14 +157,19 @@ describe("project commands", () => { status: "not-linked", }, items: expect.arrayContaining([ - expect.objectContaining({ name: "Acme Dashboard", id: "proj_123", status: null }), + expect.objectContaining({ + name: "Acme Dashboard", + id: "proj_123", + status: null, + }), ]), }); expect(payload.nextActions).toEqual([ expect.objectContaining({ kind: "user-choice", journey: "project-setup", - label: "Ask the user whether to link an existing Project or create a new one", + label: + "Ask the user whether to link an existing Project or create a new one", }), expect.objectContaining({ kind: "run-command", @@ -191,8 +218,12 @@ describe("project commands", () => { expect(result.exitCode).toBe(0); expect(result.stdout).toBe(""); expect(stderr).toContain("Which Project should this directory use?"); - expect(stderr).toContain(`Linked "./${path.basename(cwd)}" to Project "Acme Dashboard"`); - await expect(readFile(path.join(cwd, ".prisma/local.json"), "utf8")).resolves.toContain('"projectId": "proj_123"'); + expect(stderr).toContain( + `Linked "./${path.basename(cwd)}" to Project "Acme Dashboard"`, + ); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).resolves.toContain('"projectId": "proj_123"'); }); it("returns LOCAL_STATE_WRITE_FAILED when project link cannot save the local pin", async () => { @@ -240,7 +271,9 @@ describe("project commands", () => { expect(result.exitCode).toBe(0); expect(stderr).toContain("Acme Dashboard (proj_123)"); expect(stderr).toContain("Acme Dashboard (proj_321)"); - await expect(readFile(path.join(cwd, ".prisma/local.json"), "utf8")).resolves.toContain('"projectId": "proj_123"'); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).resolves.toContain('"projectId": "proj_123"'); }); it("lets the user cancel bare project link without writing local state", async () => { @@ -260,7 +293,9 @@ describe("project commands", () => { expect(result.exitCode).toBe(2); expect(stderr).toContain("Project setup canceled"); - await expect(readFile(path.join(cwd, ".prisma/local.json"), "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).rejects.toMatchObject({ code: "ENOENT" }); }); it("returns PROJECT_LINK_TARGET_REQUIRED for bare project link in JSON mode", async () => { @@ -298,7 +333,8 @@ describe("project commands", () => { expect.objectContaining({ kind: "user-choice", journey: "project-setup", - label: "Ask the user whether to link an existing Project or create a new one", + label: + "Ask the user whether to link an existing Project or create a new one", }), expect.objectContaining({ kind: "run-command", @@ -309,7 +345,9 @@ describe("project commands", () => { command: "prisma-cli project create pear", }), ]); - await expect(readFile(path.join(cwd, ".prisma/local.json"), "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).rejects.toMatchObject({ code: "ENOENT" }); }); it("does not let --yes choose a Project for bare project link", async () => { @@ -330,7 +368,9 @@ describe("project commands", () => { expect(result.exitCode).toBe(2); expect(stderr).toContain("PROJECT_LINK_TARGET_REQUIRED"); expect(stderr).not.toContain("Which Project should this directory use?"); - await expect(readFile(path.join(cwd, ".prisma/local.json"), "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).rejects.toMatchObject({ code: "ENOENT" }); }); it("shows unbound suggestions from package.json in JSON mode", async () => { @@ -415,12 +455,18 @@ describe("project commands", () => { stateDir, fixturePath, }); - const state = JSON.parse(await readFile(path.join(stateDir, "state.json"), "utf8")); + const state = JSON.parse( + await readFile(path.join(stateDir, "state.json"), "utf8"), + ); expect(result.exitCode).toBe(0); - expect(JSON.parse(result.stdout).result.resolution.projectSource).toBe("explicit"); + expect(JSON.parse(result.stdout).result.resolution.projectSource).toBe( + "explicit", + ); expect(state.project?.lastResolved ?? null).toBe(null); - await expect(readFile(path.join(cwd, ".prisma/local.json"), "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).rejects.toMatchObject({ code: "ENOENT" }); }); it("shows the pinned project from local state", async () => { @@ -615,12 +661,14 @@ describe("project commands", () => { suggestedProjectNameSource: "directory-name", candidates: [], }); - expect(payload.nextActions).toEqual(expect.arrayContaining([ - expect.objectContaining({ - kind: "user-choice", - journey: "project-setup", - }), - ])); + expect(payload.nextActions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + kind: "user-choice", + journey: "project-setup", + }), + ]), + ); }); it("uses the directory name as the suggestion when package metadata is unusable", async () => { @@ -662,9 +710,13 @@ describe("project commands", () => { const stderr = stripAnsi(result.stderr); expect(result.exitCode).toBe(0); - expect(stderr).toContain("This directory is not linked to a Prisma Project."); + expect(stderr).toContain( + "This directory is not linked to a Prisma Project.", + ); expect(stderr).toContain("project: Not linked"); - expect(stderr).toContain("Link an existing Project you choose: prisma-cli project link "); + expect(stderr).toContain( + "Link an existing Project you choose: prisma-cli project link ", + ); expect(stderr).not.toContain("match:"); expect(stderr).not.toContain("Select a project"); }); @@ -688,7 +740,9 @@ describe("project commands", () => { expect(stderr).toContain("project: Not linked"); expect(stderr).not.toContain("apple"); expect(stderr).not.toContain("match:"); - expect(stderr).toContain("Create a new Project: prisma-cli project create pear"); + expect(stderr).toContain( + "Create a new Project: prisma-cli project create pear", + ); const jsonResult = await executeCli({ argv: ["project", "show", "--json"], @@ -764,13 +818,22 @@ describe("project commands", () => { await login(cwd, stateDir); const result = await executeCli({ - argv: ["git", "connect", "git@github.com:prisma/prisma-cli.git", "--project", "proj_123", "--json"], + argv: [ + "git", + "connect", + "git@github.com:prisma/prisma-cli.git", + "--project", + "proj_123", + "--json", + ], cwd, stateDir, fixturePath, }); const payload = JSON.parse(result.stdout); - const state = JSON.parse(await readFile(path.join(stateDir, "state.json"), "utf8")); + const state = JSON.parse( + await readFile(path.join(stateDir, "state.json"), "utf8"), + ); expect(result.exitCode).toBe(0); expect(result.stderr).toBe(""); @@ -795,7 +858,9 @@ describe("project commands", () => { nextSteps: [], nextActions: [], }); - expect(state.project.repositoryConnectionsByProject.proj_123.repository.fullName).toBe("prisma/prisma-cli"); + expect( + state.project.repositoryConnectionsByProject.proj_123.repository.fullName, + ).toBe("prisma/prisma-cli"); }); it("keeps fixture repository connection idempotent for the same repo", async () => { @@ -804,22 +869,40 @@ describe("project commands", () => { await login(cwd, stateDir); await executeCli({ - argv: ["git", "connect", "https://github.com/prisma/prisma-cli", "--project", "proj_123"], + argv: [ + "git", + "connect", + "https://github.com/prisma/prisma-cli", + "--project", + "proj_123", + ], cwd, stateDir, fixturePath, }); - const initialState = JSON.parse(await readFile(path.join(stateDir, "state.json"), "utf8")); - const initialConnection = initialState.project.repositoryConnectionsByProject.proj_123; + const initialState = JSON.parse( + await readFile(path.join(stateDir, "state.json"), "utf8"), + ); + const initialConnection = + initialState.project.repositoryConnectionsByProject.proj_123; const result = await executeCli({ - argv: ["git", "connect", "git@github.com:Prisma/Prisma-CLI.git", "--project", "proj_123", "--json"], + argv: [ + "git", + "connect", + "git@github.com:Prisma/Prisma-CLI.git", + "--project", + "proj_123", + "--json", + ], cwd, stateDir, fixturePath, }); const payload = JSON.parse(result.stdout); - const nextState = JSON.parse(await readFile(path.join(stateDir, "state.json"), "utf8")); + const nextState = JSON.parse( + await readFile(path.join(stateDir, "state.json"), "utf8"), + ); expect(result.exitCode).toBe(0); expect(payload).toMatchObject({ @@ -827,7 +910,9 @@ describe("project commands", () => { command: "git.connect", }); expect(payload.result.repositoryConnection).toEqual(initialConnection); - expect(nextState.project.repositoryConnectionsByProject.proj_123).toEqual(initialConnection); + expect(nextState.project.repositoryConnectionsByProject.proj_123).toEqual( + initialConnection, + ); }); it("blocks fixture repository replacement without disconnecting first", async () => { @@ -836,21 +921,39 @@ describe("project commands", () => { await login(cwd, stateDir); await executeCli({ - argv: ["git", "connect", "https://github.com/prisma/prisma-cli", "--project", "proj_123"], + argv: [ + "git", + "connect", + "https://github.com/prisma/prisma-cli", + "--project", + "proj_123", + ], cwd, stateDir, fixturePath, }); - const initialState = JSON.parse(await readFile(path.join(stateDir, "state.json"), "utf8")); - const initialConnection = initialState.project.repositoryConnectionsByProject.proj_123; + const initialState = JSON.parse( + await readFile(path.join(stateDir, "state.json"), "utf8"), + ); + const initialConnection = + initialState.project.repositoryConnectionsByProject.proj_123; const result = await executeCli({ - argv: ["git", "connect", "https://github.com/prisma/other", "--project", "proj_123", "--json"], + argv: [ + "git", + "connect", + "https://github.com/prisma/other", + "--project", + "proj_123", + "--json", + ], cwd, stateDir, fixturePath, }); - const state = JSON.parse(await readFile(path.join(stateDir, "state.json"), "utf8")); + const state = JSON.parse( + await readFile(path.join(stateDir, "state.json"), "utf8"), + ); expect(result.exitCode).toBe(1); expect(JSON.parse(result.stdout)).toMatchObject({ @@ -860,7 +963,9 @@ describe("project commands", () => { code: "REPO_ALREADY_CONNECTED", }, }); - expect(state.project.repositoryConnectionsByProject.proj_123).toEqual(initialConnection); + expect(state.project.repositoryConnectionsByProject.proj_123).toEqual( + initialConnection, + ); }); it("disconnects a GitHub repository from an explicit project in fixture mode", async () => { @@ -868,7 +973,13 @@ describe("project commands", () => { const stateDir = path.join(cwd, ".state"); await login(cwd, stateDir); await executeCli({ - argv: ["git", "connect", "https://github.com/prisma/prisma-cli", "--project", "proj_123"], + argv: [ + "git", + "connect", + "https://github.com/prisma/prisma-cli", + "--project", + "proj_123", + ], cwd, stateDir, fixturePath, @@ -880,7 +991,9 @@ describe("project commands", () => { stateDir, fixturePath, }); - const state = JSON.parse(await readFile(path.join(stateDir, "state.json"), "utf8")); + const state = JSON.parse( + await readFile(path.join(stateDir, "state.json"), "utf8"), + ); expect(result.exitCode).toBe(0); expect(JSON.parse(result.stdout)).toMatchObject({ @@ -896,7 +1009,9 @@ describe("project commands", () => { warnings: [], nextSteps: [], }); - expect(state.project.repositoryConnectionsByProject.proj_123).toBeUndefined(); + expect( + state.project.repositoryConnectionsByProject.proj_123, + ).toBeUndefined(); }); it("returns REPO_PROVIDER_UNSUPPORTED for non-GitHub repository URLs", async () => { @@ -905,14 +1020,23 @@ describe("project commands", () => { await login(cwd, stateDir); const result = await executeCli({ - argv: ["git", "connect", "https://gitlab.com/prisma/prisma-cli", "--project", "proj_123", "--json"], + argv: [ + "git", + "connect", + "https://gitlab.com/prisma/prisma-cli", + "--project", + "proj_123", + "--json", + ], cwd, stateDir, fixturePath, }); expect(result.exitCode).toBe(2); - expect(JSON.parse(result.stdout).error.code).toBe("REPO_PROVIDER_UNSUPPORTED"); + expect(JSON.parse(result.stdout).error.code).toBe( + "REPO_PROVIDER_UNSUPPORTED", + ); }); it("returns PROJECT_SETUP_REQUIRED for repository connection without a Project binding", async () => { @@ -921,7 +1045,12 @@ describe("project commands", () => { await login(cwd, stateDir); const result = await executeCli({ - argv: ["git", "connect", "https://github.com/prisma/prisma-cli", "--json"], + argv: [ + "git", + "connect", + "https://github.com/prisma/prisma-cli", + "--json", + ], cwd, stateDir, fixturePath, @@ -977,19 +1106,29 @@ describe("project commands", () => { stateDir, fixturePath, }); - const stderr = stripAnsi(`${projectHelp.stderr}\n${showHelp.stderr}\n${createHelp.stderr}\n${linkHelp.stderr}\n${gitHelp.stderr}\n${connectRepoHelp.stderr}\n${disconnectRepoHelp.stderr}`); + const stderr = stripAnsi( + `${projectHelp.stderr}\n${showHelp.stderr}\n${createHelp.stderr}\n${linkHelp.stderr}\n${gitHelp.stderr}\n${connectRepoHelp.stderr}\n${disconnectRepoHelp.stderr}`, + ); expect(projectHelp.exitCode).toBe(0); expect(createHelp.exitCode).toBe(0); expect(linkHelp.exitCode).toBe(0); expect(gitHelp.exitCode).toBe(0); - expect(stderr).toContain("project → Manage and inspect your Prisma projects"); - expect(stderr).toContain("git → Manage Git repository connections for a project"); + expect(stderr).toContain( + "project → Manage and inspect your Prisma projects", + ); + expect(stderr).toContain( + "git → Manage Git repository connections for a project", + ); expect(stderr).toContain("Show this directory's Project binding"); expect(stderr).toContain("Create a Project and link this directory"); expect(stderr).toContain("Link this directory to a Project"); - expect(stderr).toContain("Connect the resolved project to a GitHub repository"); - expect(stderr).toContain("Disconnect the GitHub repository from the resolved project"); + expect(stderr).toContain( + "Connect the resolved project to a GitHub repository", + ); + expect(stderr).toContain( + "Disconnect the GitHub repository from the resolved project", + ); }); it("registers project env remove and rm alias help", async () => { diff --git a/packages/cli/tests/publish-prep.test.ts b/packages/cli/tests/publish-prep.test.ts index 257a668..b2b9f31 100644 --- a/packages/cli/tests/publish-prep.test.ts +++ b/packages/cli/tests/publish-prep.test.ts @@ -25,7 +25,8 @@ describe("prepare cli publish", () => { name: "@prisma/cli", private: true, version: "3.0.0-development", - description: "Command-line interface for the Prisma Developer Platform.", + description: + "Command-line interface for the Prisma Developer Platform.", type: "module", engines: { node: ">=20", @@ -50,11 +51,21 @@ describe("prepare cli publish", () => { ), "utf8", ); - await writeFile(path.join(sourceDir, "README.md"), "# Test package\n", "utf8"); - await writeFile(path.join(sourceDir, "dist/cli.js"), "#!/usr/bin/env node\nconsole.log('ok')\n", "utf8"); + await writeFile( + path.join(sourceDir, "README.md"), + "# Test package\n", + "utf8", + ); + await writeFile( + path.join(sourceDir, "dist/cli.js"), + "#!/usr/bin/env node\nconsole.log('ok')\n", + "utf8", + ); const stagedPath = await stageCliPublishPackage({ sourceDir, outputDir }); - const manifest = JSON.parse(await readFile(path.join(stagedPath, "package.json"), "utf8")); + const manifest = JSON.parse( + await readFile(path.join(stagedPath, "package.json"), "utf8"), + ); expect(stagedPath).toBe(outputDir); expect(manifest).toEqual({ @@ -103,7 +114,8 @@ describe("prepare cli publish", () => { { name: "@prisma/cli", version: "3.0.0-development", - description: "Command-line interface for the Prisma Developer Platform.", + description: + "Command-line interface for the Prisma Developer Platform.", type: "module", dependencies: {}, }, @@ -112,15 +124,25 @@ describe("prepare cli publish", () => { ), "utf8", ); - await writeFile(path.join(sourceDir, "README.md"), "# Test package\n", "utf8"); - await writeFile(path.join(sourceDir, "dist/cli.js"), "#!/usr/bin/env node\nconsole.log('ok')\n", "utf8"); + await writeFile( + path.join(sourceDir, "README.md"), + "# Test package\n", + "utf8", + ); + await writeFile( + path.join(sourceDir, "dist/cli.js"), + "#!/usr/bin/env node\nconsole.log('ok')\n", + "utf8", + ); const stagedPath = await stageCliPublishPackage({ sourceDir, outputDir, publishVersion: "3.0.0-beta.0", }); - const manifest = JSON.parse(await readFile(path.join(stagedPath, "package.json"), "utf8")); + const manifest = JSON.parse( + await readFile(path.join(stagedPath, "package.json"), "utf8"), + ); expect(manifest.version).toBe("3.0.0-beta.0"); }); @@ -141,7 +163,8 @@ describe("prepare cli publish", () => { { name: "@prisma/cli", version: "3.0.0-development", - description: "Command-line interface for the Prisma Developer Platform.", + description: + "Command-line interface for the Prisma Developer Platform.", type: "module", dependencies: {}, }, @@ -150,17 +173,38 @@ describe("prepare cli publish", () => { ), "utf8", ); - await writeFile(path.join(sourceDir, "README.md"), "# Test package\n", "utf8"); - await writeFile(path.join(sourceDir, "dist/cli.js"), "#!/usr/bin/env node\nconsole.log('ok')\n", "utf8"); + await writeFile( + path.join(sourceDir, "README.md"), + "# Test package\n", + "utf8", + ); + await writeFile( + path.join(sourceDir, "dist/cli.js"), + "#!/usr/bin/env node\nconsole.log('ok')\n", + "utf8", + ); await writeFile(path.join(sourceDir, "src/cli.ts"), "export {}\n", "utf8"); - await writeFile(path.join(sourceDir, "tests/cli.test.ts"), "export {}\n", "utf8"); - await writeFile(path.join(sourceDir, "fixtures/mock-api.json"), "{}\n", "utf8"); + await writeFile( + path.join(sourceDir, "tests/cli.test.ts"), + "export {}\n", + "utf8", + ); + await writeFile( + path.join(sourceDir, "fixtures/mock-api.json"), + "{}\n", + "utf8", + ); const stagedPath = await stageCliPublishPackage({ sourceDir, outputDir }); const topLevelFiles = await readdir(stagedPath); const distFiles = await readdir(path.join(stagedPath, "dist")); - expect(topLevelFiles.sort()).toEqual(["LICENSE", "README.md", "dist", "package.json"]); + expect(topLevelFiles.sort()).toEqual([ + "LICENSE", + "README.md", + "dist", + "package.json", + ]); expect(distFiles).toEqual(["cli.js"]); }); }); diff --git a/packages/cli/tests/resolve-cli-version.test.ts b/packages/cli/tests/resolve-cli-version.test.ts index 74550d0..f9c1452 100644 --- a/packages/cli/tests/resolve-cli-version.test.ts +++ b/packages/cli/tests/resolve-cli-version.test.ts @@ -12,7 +12,10 @@ import { } from "../../../scripts/resolve-cli-version.mjs"; const execFileAsync = promisify(execFile); -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../..", +); const scriptPath = path.join(repoRoot, "scripts/resolve-cli-version.mjs"); describe("resolve cli version", () => { @@ -32,17 +35,21 @@ describe("resolve cli version", () => { }); it("computes a unique dev build version", () => { - expect(resolveDevVersion({ - runNumber: "123", - runAttempt: "2", - })).toBe("3.0.0-dev.123.2"); + expect( + resolveDevVersion({ + runNumber: "123", + runAttempt: "2", + }), + ).toBe("3.0.0-dev.123.2"); }); it("computes an exact PR preview version", () => { - expect(resolvePrVersion({ - prNumber: "43", - sha: "f1110dd704a9382c429b", - })).toBe("3.0.0-pr.43.shaf1110dd704a9"); + expect( + resolvePrVersion({ + prNumber: "43", + sha: "f1110dd704a9382c429b", + }), + ).toBe("3.0.0-pr.43.shaf1110dd704a9"); }); it("prints GitHub output lines for the next beta command", async () => { diff --git a/packages/cli/tests/shell.test.ts b/packages/cli/tests/shell.test.ts index 88a7d91..228e3cf 100644 --- a/packages/cli/tests/shell.test.ts +++ b/packages/cli/tests/shell.test.ts @@ -21,8 +21,12 @@ describe("shell behavior", () => { const error = new Error("boom"); error.stack = "Error: boom\n at explode"; - expect(formatUnexpectedError(error, false)).toContain("Unexpected CLI error: boom"); - expect(formatUnexpectedError(error, false)).toContain("More: Re-run with --trace"); + expect(formatUnexpectedError(error, false)).toContain( + "Unexpected CLI error: boom", + ); + expect(formatUnexpectedError(error, false)).toContain( + "More: Re-run with --trace", + ); expect(formatUnexpectedError(error, false)).not.toContain("at explode"); expect(formatUnexpectedError(error, true)).toContain("at explode"); }); @@ -39,7 +43,9 @@ describe("shell behavior", () => { }); expect(result.exitCode).toBe(0); - expect(result.stderr).toContain("prisma → The Prisma Developer Platform, from your terminal"); + expect(result.stderr).toContain( + "prisma → The Prisma Developer Platform, from your terminal", + ); expect(result.stderr).toContain("auth"); expect(result.stderr).toContain("project"); expect(result.stderr).toContain("Global options:"); @@ -50,9 +56,13 @@ describe("shell behavior", () => { expect(result.stderr).not.toContain("--color"); expect(result.stderr).toContain("$ prisma-cli auth login"); - const commandMatch = result.stderr.match(/app\s+Manage apps and deployments for a project/); + const commandMatch = result.stderr.match( + /app\s+Manage apps and deployments for a project/, + ); const commandIndex = commandMatch?.index ?? -1; - const descriptionIndex = result.stderr.indexOf("Deploy your app with isolated infrastructure for every branch"); + const descriptionIndex = result.stderr.indexOf( + "Deploy your app with isolated infrastructure for every branch", + ); const globalOptionsIndex = result.stderr.indexOf("Global options:"); const examplesIndex = result.stderr.indexOf("Examples:"); @@ -98,10 +108,14 @@ describe("shell behavior", () => { }); expect(rootResult.exitCode).toBe(0); - expect(rootResult.stderr).toContain("prisma → The Prisma Developer Platform, from your terminal"); + expect(rootResult.stderr).toContain( + "prisma → The Prisma Developer Platform, from your terminal", + ); expect(authResult.exitCode).toBe(0); - expect(authResult.stderr).toContain("auth → Manage local authentication for the CLI"); + expect(authResult.stderr).toContain( + "auth → Manage local authentication for the CLI", + ); expect(authResult.stderr).toContain("Global options:"); expect(authResult.stderr).toContain("--json"); expect(authResult.stderr).toContain("--no-interactive"); @@ -109,15 +123,21 @@ describe("shell behavior", () => { expect(authResult.stderr).not.toContain("--color"); expect(projectResult.exitCode).toBe(0); - expect(projectResult.stderr).toContain("project → Manage and inspect your Prisma projects"); + expect(projectResult.stderr).toContain( + "project → Manage and inspect your Prisma projects", + ); expect(projectResult.stderr).toContain("Global options:"); expect(branchResult.exitCode).toBe(0); - expect(branchResult.stderr).toContain("branch → View your Platform branches"); + expect(branchResult.stderr).toContain( + "branch → View your Platform branches", + ); expect(branchResult.stderr).toContain("Global options:"); expect(databaseResult.exitCode).toBe(0); - expect(databaseResult.stderr).toContain("database → Manage Prisma Postgres databases for a project"); + expect(databaseResult.stderr).toContain( + "database → Manage Prisma Postgres databases for a project", + ); expect(databaseResult.stderr).toContain("Global options:"); }); diff --git a/packages/cli/tests/token-storage.test.ts b/packages/cli/tests/token-storage.test.ts index 1d5e78e..90f5eb1 100644 --- a/packages/cli/tests/token-storage.test.ts +++ b/packages/cli/tests/token-storage.test.ts @@ -6,7 +6,10 @@ import { describe, expect, it } from "vitest"; import { FileTokenStorage } from "../src/adapters/token-storage"; import { createTempCwd } from "./helpers"; -async function writeAuthFile(authFilePath: string, tokens: unknown[]): Promise { +async function writeAuthFile( + authFilePath: string, + tokens: unknown[], +): Promise { await fs.mkdir(path.dirname(authFilePath), { recursive: true }); await fs.writeFile(authFilePath, JSON.stringify({ tokens }, null, 2)); } @@ -98,9 +101,12 @@ describe("FileTokenStorage", () => { PRISMA_COMPUTE_AUTH_FILE: authFilePath, } as NodeJS.ProcessEnv); const controller = new AbortController(); - const secondStorage = new FileTokenStorage({ - PRISMA_COMPUTE_AUTH_FILE: authFilePath, - } as NodeJS.ProcessEnv, controller.signal); + const secondStorage = new FileTokenStorage( + { + PRISMA_COMPUTE_AUTH_FILE: authFilePath, + } as NodeJS.ProcessEnv, + controller.signal, + ); const reason = new Error("cancelled"); let releaseFirst!: () => void; const firstReleased = new Promise((resolve) => { diff --git a/packages/cli/tests/update-check.test.ts b/packages/cli/tests/update-check.test.ts index e5dd80b..058aa44 100644 --- a/packages/cli/tests/update-check.test.ts +++ b/packages/cli/tests/update-check.test.ts @@ -3,7 +3,11 @@ import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { describe, expect, it } from "vitest"; import { getCliVersion } from "../src/lib/version"; -import { runUpdateDiscovery, selectUpdateInstruction, UpdateCheckStore } from "../src/shell/update-check"; +import { + runUpdateDiscovery, + selectUpdateInstruction, + UpdateCheckStore, +} from "../src/shell/update-check"; import { createTempCwd, executeCli } from "./helpers"; const fixturePath = path.resolve("fixtures/mock-api.json"); @@ -24,9 +28,15 @@ describe("automatic update check", () => { expect(result.exitCode).toBe(0); expect(result.stdout).toBe(""); - expect(result.stderr).toContain(`Update available: prisma-cli ${getCliVersion()} -> ${nextMajorVersion()}`); - expect(result.stderr).toContain("See https://www.prisma.io/docs/orm/tools/prisma-cli for update instructions."); - expect(result.stderr.indexOf("Update available")).toBeLessThan(result.stderr.indexOf("auth whoami")); + expect(result.stderr).toContain( + `Update available: prisma-cli ${getCliVersion()} -> ${nextMajorVersion()}`, + ); + expect(result.stderr).toContain( + "See https://www.prisma.io/docs/orm/tools/prisma-cli for update instructions.", + ); + expect(result.stderr.indexOf("Update available")).toBeLessThan( + result.stderr.indexOf("auth whoami"), + ); }); it("continues with the original command result when the command fails", async () => { @@ -71,12 +81,47 @@ describe("automatic update check", () => { }); it.each([ - { name: "quiet mode", argv: ["auth", "whoami", "--quiet"], env: {}, isTTY: true, preserveCI: false }, - { name: "CI", argv: ["auth", "whoami"], env: { CI: "1" }, isTTY: true, preserveCI: true }, - { name: "non-TTY", argv: ["auth", "whoami"], env: {}, isTTY: false, preserveCI: false }, - { name: "opt-out", argv: ["auth", "whoami"], env: { NO_UPDATE_NOTIFIER: "1" }, isTTY: true, preserveCI: false }, - { name: "version flag", argv: ["--version"], env: {}, isTTY: true, preserveCI: false }, - ])("suppresses cached update notices for $name", async ({ argv, env, isTTY, preserveCI }) => { + { + name: "quiet mode", + argv: ["auth", "whoami", "--quiet"], + env: {}, + isTTY: true, + preserveCI: false, + }, + { + name: "CI", + argv: ["auth", "whoami"], + env: { CI: "1" }, + isTTY: true, + preserveCI: true, + }, + { + name: "non-TTY", + argv: ["auth", "whoami"], + env: {}, + isTTY: false, + preserveCI: false, + }, + { + name: "opt-out", + argv: ["auth", "whoami"], + env: { NO_UPDATE_NOTIFIER: "1" }, + isTTY: true, + preserveCI: false, + }, + { + name: "version flag", + argv: ["--version"], + env: {}, + isTTY: true, + preserveCI: false, + }, + ])("suppresses cached update notices for $name", async ({ + argv, + env, + isTTY, + preserveCI, + }) => { const { cwd, stateDir, updateCheckDir } = await createUpdateCheckTestDirs(); await seedStaleUpdate(updateCheckDir); @@ -111,13 +156,19 @@ describe("automatic update check", () => { }); expect(result.stderr).toContain("Update available"); - await expect(access(path.join(stateDir, "update-check.json"))).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + access(path.join(stateDir, "update-check.json")), + ).rejects.toMatchObject({ code: "ENOENT" }); }); it("continues with the original command result when cached update state is unreadable", async () => { const { cwd, stateDir, updateCheckDir } = await createUpdateCheckTestDirs(); await mkdir(updateCheckDir, { recursive: true }); - await writeFile(path.join(updateCheckDir, "update-check.json"), "{not json", "utf8"); + await writeFile( + path.join(updateCheckDir, "update-check.json"), + "{not json", + "utf8", + ); const result = await executeCli({ argv: ["auth", "whoami"], @@ -197,7 +248,9 @@ describe("automatic update check", () => { env: enableUpdateCheck(updateCheckDir), }); - await expect(access(path.join(updateCheckDir, "update-check.json"))).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + access(path.join(updateCheckDir, "update-check.json")), + ).rejects.toMatchObject({ code: "ENOENT" }); }); it("skips remote discovery attempts inside the 24-hour interval", async () => { @@ -218,7 +271,9 @@ describe("automatic update check", () => { env: enableUpdateCheck(updateCheckDir), }); - expect((await readUpdateCheckState(updateCheckDir)).checkedAt).toBe(checkedAt); + expect((await readUpdateCheckState(updateCheckDir)).checkedAt).toBe( + checkedAt, + ); }); it("persists successful remote discovery results from injected registry metadata", async () => { @@ -228,7 +283,8 @@ describe("automatic update check", () => { cacheDir: updateCheckDir, installedVersion: getCliVersion(), now: new Date("2026-01-02T00:00:00.000Z"), - fetchImpl: async () => new Response(JSON.stringify({ "dist-tags": { latest: "9.8.7" } })), + fetchImpl: async () => + new Response(JSON.stringify({ "dist-tags": { latest: "9.8.7" } })), }); expect(await readUpdateCheckState(updateCheckDir)).toMatchObject({ @@ -253,7 +309,8 @@ describe("automatic update check", () => { cacheDir: updateCheckDir, installedVersion: getCliVersion(), now: new Date("2026-01-02T00:00:00.000Z"), - fetchImpl: async () => new Response(JSON.stringify({ "dist-tags": { latest: "9.8.7" } })), + fetchImpl: async () => + new Response(JSON.stringify({ "dist-tags": { latest: "9.8.7" } })), }); expect(await readUpdateCheckState(updateCheckDir)).toMatchObject({ @@ -275,7 +332,9 @@ describe("automatic update check", () => { }, }), ).resolves.toBeUndefined(); - await expect(access(path.join(updateCheckDir, "update-check.json"))).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + access(path.join(updateCheckDir, "update-check.json")), + ).rejects.toMatchObject({ code: "ENOENT" }); }); it.each([ @@ -283,23 +342,36 @@ describe("automatic update check", () => { name: "local npm", env: { npm_config_user_agent: "npm/10.9.0 node/v24.14.1 darwin arm64" }, argv: ["node", "/repo/node_modules/.bin/prisma-cli"], - expected: { type: "command", value: "npm install --save-dev @prisma/cli@latest" }, + expected: { + type: "command", + value: "npm install --save-dev @prisma/cli@latest", + }, }, { name: "global npm", - env: { npm_config_user_agent: "npm/10.9.0 node/v24.14.1 darwin arm64", npm_config_global: "true" }, + env: { + npm_config_user_agent: "npm/10.9.0 node/v24.14.1 darwin arm64", + npm_config_global: "true", + }, argv: ["node", "/usr/local/bin/prisma-cli"], - expected: { type: "command", value: "npm install --global @prisma/cli@latest" }, + expected: { + type: "command", + value: "npm install --global @prisma/cli@latest", + }, }, { name: "local pnpm", - env: { npm_config_user_agent: "pnpm/10.30.0 npm/? node/v24.14.1 darwin arm64" }, + env: { + npm_config_user_agent: "pnpm/10.30.0 npm/? node/v24.14.1 darwin arm64", + }, argv: ["node", "/repo/node_modules/.bin/prisma-cli"], expected: { type: "command", value: "pnpm add -D @prisma/cli@latest" }, }, { name: "local bun", - env: { npm_config_user_agent: "bun/1.3.0 npm/? node/v24.14.1 darwin arm64" }, + env: { + npm_config_user_agent: "bun/1.3.0 npm/? node/v24.14.1 darwin arm64", + }, argv: ["node", "/repo/node_modules/.bin/prisma-cli"], expected: { type: "command", value: "bun add -d @prisma/cli@latest" }, }, @@ -307,25 +379,40 @@ describe("automatic update check", () => { name: "npx", env: { npm_lifecycle_event: "npx" }, argv: ["node", "/Users/alice/.npm/_npx/123/node_modules/.bin/prisma-cli"], - expected: { type: "docs", value: "https://www.prisma.io/docs/orm/tools/prisma-cli" }, + expected: { + type: "docs", + value: "https://www.prisma.io/docs/orm/tools/prisma-cli", + }, }, { name: "pnpx", - env: { npm_lifecycle_event: "pnpx", npm_config_user_agent: "pnpm/10.30.0" }, + env: { + npm_lifecycle_event: "pnpx", + npm_config_user_agent: "pnpm/10.30.0", + }, argv: ["node", "/repo/node_modules/.bin/prisma-cli"], - expected: { type: "docs", value: "https://www.prisma.io/docs/orm/tools/prisma-cli" }, + expected: { + type: "docs", + value: "https://www.prisma.io/docs/orm/tools/prisma-cli", + }, }, { name: "bunx", env: { npm_config_user_agent: "bun/1.3.0" }, argv: ["node", "/Users/alice/.bun/install/cache/@prisma/cli/prisma-cli"], - expected: { type: "docs", value: "https://www.prisma.io/docs/orm/tools/prisma-cli" }, + expected: { + type: "docs", + value: "https://www.prisma.io/docs/orm/tools/prisma-cli", + }, }, { name: "unknown", env: {}, argv: ["node", "/some/path/prisma-cli"], - expected: { type: "docs", value: "https://www.prisma.io/docs/orm/tools/prisma-cli" }, + expected: { + type: "docs", + value: "https://www.prisma.io/docs/orm/tools/prisma-cli", + }, }, ])("selects update instructions for $name", ({ env, argv, expected }) => { expect(selectUpdateInstruction(env, argv)).toEqual(expected); @@ -358,7 +445,9 @@ async function seedStaleUpdate(updateCheckDir: string): Promise { } async function readUpdateCheckState(updateCheckDir: string) { - return JSON.parse(await readFile(path.join(updateCheckDir, "update-check.json"), "utf8")) as Record; + return JSON.parse( + await readFile(path.join(updateCheckDir, "update-check.json"), "utf8"), + ) as Record; } function nextMajorVersion(): string { diff --git a/packages/cli/tests/use-case-helpers.ts b/packages/cli/tests/use-case-helpers.ts index 41d08a9..be241ea 100644 --- a/packages/cli/tests/use-case-helpers.ts +++ b/packages/cli/tests/use-case-helpers.ts @@ -25,7 +25,8 @@ export async function createUseCaseGateways(options?: { identityGateway: { listProviders: () => api.listProviders(), getProvider: (providerId) => api.getProvider(providerId), - listUsersForProvider: (providerId) => api.listUsersForProvider(providerId).map(toAuthUser), + listUsersForProvider: (providerId) => + api.listUsersForProvider(providerId).map(toAuthUser), getUser: (userId) => { const user = api.getUser(userId); return user ? toAuthUser(user) : undefined; @@ -34,7 +35,8 @@ export async function createUseCaseGateways(options?: { const user = api.getUserForProvider(providerId, userId); return user ? toAuthUser(user) : undefined; }, - listUserWorkspaces: (userId) => api.listUserWorkspaces(userId).map(toAuthWorkspace), + listUserWorkspaces: (userId) => + api.listUserWorkspaces(userId).map(toAuthWorkspace), getWorkspace: (workspaceId) => { const workspace = api.getWorkspace(workspaceId); return workspace ? toAuthWorkspace(workspace) : undefined; @@ -45,12 +47,15 @@ export async function createUseCaseGateways(options?: { }, }, projectGateway: { - listProjectsForWorkspace: (workspaceId) => api.listProjectsForWorkspace(workspaceId), + listProjectsForWorkspace: (workspaceId) => + api.listProjectsForWorkspace(workspaceId), getProject: (projectId) => api.getProject(projectId), - getProjectForWorkspace: (workspaceId, projectId) => api.getProjectForWorkspace(workspaceId, projectId), + getProjectForWorkspace: (workspaceId, projectId) => + api.getProjectForWorkspace(workspaceId, projectId), }, branchGateway: { - listBranchesForProject: (projectId) => api.listBranchesForProject(projectId), + listBranchesForProject: (projectId) => + api.listBranchesForProject(projectId), getBranchForProject: (projectId, name) => { return api.getBranchForProject(projectId, name); }, diff --git a/packages/cli/tests/version.test.ts b/packages/cli/tests/version.test.ts index f9d75aa..39e55b7 100644 --- a/packages/cli/tests/version.test.ts +++ b/packages/cli/tests/version.test.ts @@ -63,10 +63,14 @@ describe("version", () => { expect(result.exitCode).toBe(0); expect(result.stdout).toBe(""); - expect(result.stderr).toContain("version → Showing CLI build and environment."); + expect(result.stderr).toContain( + "version → Showing CLI build and environment.", + ); expect(result.stderr).toContain(`prisma-cli: ${pkg.version}`); expect(result.stderr).toContain(`node: ${process.version}`); - expect(result.stderr).toContain(`os: ${process.platform} ${process.arch}`); + expect(result.stderr).toContain( + `os: ${process.platform} ${process.arch}`, + ); expect(result.stderr).toContain("invocation:"); }); @@ -104,7 +108,9 @@ describe("version", () => { nextSteps: [], nextActions: [], }); - expect(["bunx", "npx", "global", "dev", "unknown"]).toContain(payload.result.invocation); + expect(["bunx", "npx", "global", "dev", "unknown"]).toContain( + payload.result.invocation, + ); }); it("requires no auth, no project context, and no network for the subcommand", async () => { @@ -175,7 +181,8 @@ describe("version", () => { expect( detectInvocation( { - npm_config_user_agent: "npm/10.9.0 node/v24.14.1 darwin arm64 workspaces/false", + npm_config_user_agent: + "npm/10.9.0 node/v24.14.1 darwin arm64 workspaces/false", }, ["node", "/repo/node_modules/.bin/prisma-cli"], ), @@ -186,12 +193,21 @@ describe("version", () => { expect( detectInvocation( { - npm_execpath: "C:\\Users\\alice\\AppData\\Local\\npm-cache\\_npx\\1234\\node_modules\\npm\\bin\\npm-cli.js", + npm_execpath: + "C:\\Users\\alice\\AppData\\Local\\npm-cache\\_npx\\1234\\node_modules\\npm\\bin\\npm-cli.js", }, - ["node", "C:\\Users\\alice\\AppData\\Local\\npm-cache\\_npx\\1234\\node_modules\\.bin\\prisma-cli.cmd"], + [ + "node", + "C:\\Users\\alice\\AppData\\Local\\npm-cache\\_npx\\1234\\node_modules\\.bin\\prisma-cli.cmd", + ], ), ).toBe("npx"); - expect(detectInvocation({}, ["node", "C:\\Users\\alice\\AppData\\Roaming\\npm\\prisma-cli.cmd"])).toBe("global"); + expect( + detectInvocation({}, [ + "node", + "C:\\Users\\alice\\AppData\\Roaming\\npm\\prisma-cli.cmd", + ]), + ).toBe("global"); }); }); diff --git a/packages/compute/src/scale-to-zero-control.ts b/packages/compute/src/scale-to-zero-control.ts index e74131f..0e86138 100644 --- a/packages/compute/src/scale-to-zero-control.ts +++ b/packages/compute/src/scale-to-zero-control.ts @@ -47,7 +47,9 @@ function getControlFileState(): ControlFileState { return controlFileState; } -export function configureScaleToZeroControlFileForTests(path: string | undefined): void { +export function configureScaleToZeroControlFileForTests( + path: string | undefined, +): void { if (controlFileState.kind === "open") { fs.closeSync(controlFileState.fd); } diff --git a/packages/compute/tests/scale-to-zero.test.ts b/packages/compute/tests/scale-to-zero.test.ts index 591dc77..8bead5d 100644 --- a/packages/compute/tests/scale-to-zero.test.ts +++ b/packages/compute/tests/scale-to-zero.test.ts @@ -104,7 +104,9 @@ describe("scale-to-zero guard", () => { }); it("is a no-op when the control file is unavailable", async () => { - configureScaleToZeroControlFileForTests(path.join(os.tmpdir(), "missing-scale-to-zero-file")); + configureScaleToZeroControlFileForTests( + path.join(os.tmpdir(), "missing-scale-to-zero-file"), + ); const promise = Promise.resolve("done"); expect(waitUntil(promise)).toBeUndefined(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec290da..1089f6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@biomejs/biome': + specifier: 2.4.16 + version: 2.4.16 gray-matter: specifier: ^4.0.3 version: 4.0.3 @@ -137,6 +140,63 @@ packages: resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==} engines: {node: ^20.19.0 || >=22.12.0} + '@biomejs/biome@2.4.16': + resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.16': + resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.16': + resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.16': + resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.16': + resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.16': + resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.16': + resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.16': + resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.16': + resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@clack/core@1.4.0': resolution: {integrity: sha512-7Wctjq6f7c1CPz8sPpkwUnz8yRgVANkpNupb81q432FjcJg4l+Sw7XANdNSdWfAKq0IHI0JTcUeK5dxs/HrGPw==} engines: {node: '>= 20.12.0'} @@ -1452,6 +1512,41 @@ snapshots: '@babel/helper-string-parser': 8.0.0-rc.6 '@babel/helper-validator-identifier': 8.0.0-rc.3 + '@biomejs/biome@2.4.16': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.16 + '@biomejs/cli-darwin-x64': 2.4.16 + '@biomejs/cli-linux-arm64': 2.4.16 + '@biomejs/cli-linux-arm64-musl': 2.4.16 + '@biomejs/cli-linux-x64': 2.4.16 + '@biomejs/cli-linux-x64-musl': 2.4.16 + '@biomejs/cli-win32-arm64': 2.4.16 + '@biomejs/cli-win32-x64': 2.4.16 + + '@biomejs/cli-darwin-arm64@2.4.16': + optional: true + + '@biomejs/cli-darwin-x64@2.4.16': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.16': + optional: true + + '@biomejs/cli-linux-arm64@2.4.16': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.16': + optional: true + + '@biomejs/cli-linux-x64@2.4.16': + optional: true + + '@biomejs/cli-win32-arm64@2.4.16': + optional: true + + '@biomejs/cli-win32-x64@2.4.16': + optional: true + '@clack/core@1.4.0': dependencies: fast-wrap-ansi: 0.2.2 diff --git a/scripts/prepare-cli-publish.mjs b/scripts/prepare-cli-publish.mjs index fa1813b..189e02a 100644 --- a/scripts/prepare-cli-publish.mjs +++ b/scripts/prepare-cli-publish.mjs @@ -5,8 +5,10 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; export async function stageCliPublishPackage(options = {}) { - const sourceDir = options.sourceDir ?? path.join(getRepoRoot(), "packages/cli"); - const outputDir = options.outputDir ?? path.join(getRepoRoot(), ".publish/cli"); + const sourceDir = + options.sourceDir ?? path.join(getRepoRoot(), "packages/cli"); + const outputDir = + options.outputDir ?? path.join(getRepoRoot(), ".publish/cli"); const publishVersion = options.publishVersion; await ensureBuildArtifacts(sourceDir); @@ -14,9 +16,17 @@ export async function stageCliPublishPackage(options = {}) { await rm(outputDir, { recursive: true, force: true }); await mkdir(outputDir, { recursive: true }); - await cp(path.join(sourceDir, "dist"), path.join(outputDir, "dist"), { recursive: true }); - await cp(path.join(sourceDir, "README.md"), path.join(outputDir, "README.md")); - await cp(path.join(getRepoRoot(), "LICENSE"), path.join(outputDir, "LICENSE")); + await cp(path.join(sourceDir, "dist"), path.join(outputDir, "dist"), { + recursive: true, + }); + await cp( + path.join(sourceDir, "README.md"), + path.join(outputDir, "README.md"), + ); + await cp( + path.join(getRepoRoot(), "LICENSE"), + path.join(outputDir, "LICENSE"), + ); const sourceManifest = JSON.parse( await readFile(path.join(sourceDir, "package.json"), "utf8"), @@ -76,12 +86,10 @@ function removeUndefinedFields(value) { async function main() { const { outputDir, publishVersion } = parseCliArgs(process.argv.slice(2)); - const stagedPath = await stageCliPublishPackage( - { - ...(outputDir ? { outputDir: path.resolve(outputDir) } : {}), - ...(publishVersion ? { publishVersion } : {}), - }, - ); + const stagedPath = await stageCliPublishPackage({ + ...(outputDir ? { outputDir: path.resolve(outputDir) } : {}), + ...(publishVersion ? { publishVersion } : {}), + }); process.stdout.write(`${stagedPath}\n`); } @@ -125,9 +133,13 @@ function parseCliArgs(args) { return { outputDir, publishVersion }; } -if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { +if ( + process.argv[1] && + fileURLToPath(import.meta.url) === path.resolve(process.argv[1]) +) { main().catch((error) => { - const message = error instanceof Error ? error.stack ?? error.message : String(error); + const message = + error instanceof Error ? (error.stack ?? error.message) : String(error); process.stderr.write(`${message}\n`); process.exitCode = 1; }); diff --git a/scripts/resolve-cli-version.mjs b/scripts/resolve-cli-version.mjs index 6d15306..d56ffd8 100644 --- a/scripts/resolve-cli-version.mjs +++ b/scripts/resolve-cli-version.mjs @@ -82,18 +82,22 @@ function main() { const options = parseOptions(args); if (command === "dev") { - process.stdout.write(`version=${resolveDevVersion({ - runNumber: options["run-number"], - runAttempt: options["run-attempt"], - })}\n`); + process.stdout.write( + `version=${resolveDevVersion({ + runNumber: options["run-number"], + runAttempt: options["run-attempt"], + })}\n`, + ); return; } if (command === "pr") { - process.stdout.write(`version=${resolvePrVersion({ - prNumber: options["pr-number"], - sha: options.sha, - })}\n`); + process.stdout.write( + `version=${resolvePrVersion({ + prNumber: options["pr-number"], + sha: options.sha, + })}\n`, + ); return; } @@ -104,10 +108,15 @@ function main() { return; } - throw new Error("Usage: resolve-cli-version.mjs [options]"); + throw new Error( + "Usage: resolve-cli-version.mjs [options]", + ); } -if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { +if ( + process.argv[1] && + fileURLToPath(import.meta.url) === path.resolve(process.argv[1]) +) { try { main(); } catch (error) { diff --git a/scripts/smoke-cli-nextjs-artifact.mjs b/scripts/smoke-cli-nextjs-artifact.mjs index 0aac268..2bb604e 100644 --- a/scripts/smoke-cli-nextjs-artifact.mjs +++ b/scripts/smoke-cli-nextjs-artifact.mjs @@ -6,7 +6,10 @@ import { spawn } from "node:child_process"; import path from "node:path"; import { fileURLToPath } from "node:url"; -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", +); const cliPath = path.join(repoRoot, "packages/cli/dist/cli.js"); const fixturePath = path.join(repoRoot, "examples/next-smoke"); @@ -24,7 +27,9 @@ try { "node_modules/next/dist/shared/lib/constants.js", ); const requireFromNext = createRequire(constantsPath); - const resolved = requireFromNext.resolve("@swc/helpers/_/_interop_require_default"); + const resolved = requireFromNext.resolve( + "@swc/helpers/_/_interop_require_default", + ); process.stdout.write(`Next.js artifact smoke passed: ${resolved}\n`); } finally { @@ -38,7 +43,9 @@ async function runFixtureInstall() { }); if (exit !== 0) { - throw new Error(`Next.js smoke fixture install failed with exit code ${exit}`); + throw new Error( + `Next.js smoke fixture install failed with exit code ${exit}`, + ); } } diff --git a/scripts/validate-skills.mjs b/scripts/validate-skills.mjs index f737fb1..52e4d0b 100644 --- a/scripts/validate-skills.mjs +++ b/scripts/validate-skills.mjs @@ -23,7 +23,8 @@ export function validateSkillMd(content) { try { ({ data } = matter(content)); } catch (error) { - const message = error instanceof Error ? error.message.split("\n")[0] : String(error); + const message = + error instanceof Error ? error.message.split("\n")[0] : String(error); errors.push(`frontmatter parse error: ${message}`); return errors; } @@ -33,7 +34,9 @@ export function validateSkillMd(content) { } if (typeof data.description !== "string" || !data.description.trim()) { - errors.push("missing or invalid 'description' (must be a non-empty string)"); + errors.push( + "missing or invalid 'description' (must be a non-empty string)", + ); } else if (data.description.length > MAX_DESCRIPTION_LENGTH) { errors.push( `description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${data.description.length}); use a folded block scalar (description: >) or shorten the text`, @@ -88,9 +91,15 @@ export function validateSkillFile(filePath, root) { }; } -export function runCheck({ root = repoRoot, skillsDir = SKILLS_DIR, files } = {}) { +export function runCheck({ + root = repoRoot, + skillsDir = SKILLS_DIR, + files, +} = {}) { if (files?.length) { - return files.map((file) => validateSkillFile(file, root)).filter((offence) => offence !== null); + return files + .map((file) => validateSkillFile(file, root)) + .filter((offence) => offence !== null); } const offences = []; diff --git a/scripts/validate-skills.test.mjs b/scripts/validate-skills.test.mjs index bc26616..82bb321 100644 --- a/scripts/validate-skills.test.mjs +++ b/scripts/validate-skills.test.mjs @@ -3,7 +3,11 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, normalize } from "node:path"; import { describe, it } from "node:test"; -import { MAX_DESCRIPTION_LENGTH, runCheck, validateSkillMd } from "./validate-skills.mjs"; +import { + MAX_DESCRIPTION_LENGTH, + runCheck, + validateSkillMd, +} from "./validate-skills.mjs"; const validSkill = `--- name: example-skill @@ -49,7 +53,9 @@ description: ${longDescription} }); it("fails when frontmatter is missing", () => { - deepStrictEqual(validateSkillMd("# No frontmatter\n"), ["missing frontmatter block"]); + deepStrictEqual(validateSkillMd("# No frontmatter\n"), [ + "missing frontmatter block", + ]); }); }); @@ -58,7 +64,10 @@ describe("runCheck", () => { const root = mkdtempSync(join(tmpdir(), "validate-skills-")); const skillDir = join(root, "skills", "good-skill"); mkdirSync(skillDir, { recursive: true }); - writeFileSync(join(skillDir, "SKILL.md"), validSkill.replace("example-skill", "good-skill")); + writeFileSync( + join(skillDir, "SKILL.md"), + validSkill.replace("example-skill", "good-skill"), + ); deepStrictEqual(runCheck({ root }), []); }); @@ -80,8 +89,14 @@ description: broken yaml: this colon breaks parsing const offences = runCheck({ root }); strictEqual(offences.length, 1); - strictEqual(normalize(offences[0].file), normalize(join("skills", "bad-skill", "SKILL.md"))); - strictEqual(offences[0].errors[0].startsWith("frontmatter parse error:"), true); + strictEqual( + normalize(offences[0].file), + normalize(join("skills", "bad-skill", "SKILL.md")), + ); + strictEqual( + offences[0].errors[0].startsWith("frontmatter parse error:"), + true, + ); }); it("reports unreadable explicit files without aborting", () => { @@ -91,7 +106,10 @@ description: broken yaml: this colon breaks parsing const offences = runCheck({ root, files: [missingSkill] }); strictEqual(offences.length, 1); - strictEqual(normalize(offences[0].file), normalize(join("missing", "SKILL.md"))); + strictEqual( + normalize(offences[0].file), + normalize(join("missing", "SKILL.md")), + ); strictEqual(offences[0].errors[0].startsWith("Unable to read file:"), true); }); }); From c2f76e8d2d5800daad310e188b1493b1c59c06c9 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:24:30 -0400 Subject: [PATCH 02/28] biome recommended settings --- biome.jsonc | 2 +- packages/cli/src/adapters/mock-api.ts | 2 +- packages/cli/src/adapters/token-storage.ts | 4 +- packages/cli/src/cli.ts | 2 +- packages/cli/src/commands/app/index.ts | 14 +- packages/cli/src/commands/auth/index.ts | 6 +- packages/cli/src/commands/branch/index.ts | 4 +- packages/cli/src/commands/database/index.ts | 2 +- packages/cli/src/commands/env.ts | 2 +- packages/cli/src/commands/git/index.ts | 4 +- packages/cli/src/commands/project/index.ts | 8 +- packages/cli/src/commands/version/index.ts | 2 +- packages/cli/src/controllers/app-env-file.ts | 2 +- packages/cli/src/controllers/app-env.ts | 18 +- packages/cli/src/controllers/app.ts | 163 +++++++++--------- packages/cli/src/controllers/auth.ts | 14 +- packages/cli/src/controllers/branch.ts | 13 +- packages/cli/src/controllers/database.ts | 4 +- packages/cli/src/controllers/project.ts | 20 +-- .../cli/src/lib/app/branch-database-deploy.ts | 19 +- packages/cli/src/lib/app/local-dev.ts | 11 +- .../cli/src/lib/app/preview-build-settings.ts | 4 +- packages/cli/src/lib/app/preview-build.ts | 14 +- packages/cli/src/lib/app/preview-progress.ts | 5 +- packages/cli/src/lib/app/preview-provider.ts | 14 +- .../cli/src/lib/app/production-deploy-gate.ts | 2 +- packages/cli/src/lib/auth/login.ts | 6 +- packages/cli/src/lib/diagnostics.ts | 2 +- .../cli/src/lib/project/interactive-setup.ts | 8 +- packages/cli/src/lib/project/resolution.ts | 8 +- packages/cli/src/lib/project/setup.ts | 9 +- packages/cli/src/presenters/app-env.ts | 6 +- packages/cli/src/presenters/app.ts | 12 +- packages/cli/src/presenters/auth.ts | 2 +- packages/cli/src/presenters/database.ts | 2 +- packages/cli/src/presenters/project.ts | 15 +- packages/cli/src/shell/command-runner.ts | 4 +- packages/cli/src/shell/global-flags.ts | 2 +- packages/cli/src/shell/help.ts | 2 +- packages/cli/src/shell/prompt.ts | 2 +- packages/cli/src/shell/runtime.ts | 4 +- packages/cli/src/shell/ui.ts | 2 +- packages/cli/src/shell/update-check.ts | 2 +- packages/cli/src/use-cases/branch.ts | 4 +- packages/cli/tests/app-bun-compat.test.ts | 6 +- packages/cli/tests/app-env-presenter.test.ts | 2 +- packages/cli/tests/app-env-vars.test.ts | 2 +- packages/cli/tests/app-presenter.test.ts | 3 +- packages/cli/tests/app-provider.test.ts | 4 +- packages/cli/tests/app-state.test.ts | 2 +- packages/cli/tests/auth-login.test.ts | 4 +- packages/cli/tests/auth-ops.test.ts | 4 +- packages/cli/tests/auth.test.ts | 2 +- packages/cli/tests/branch.test.ts | 6 +- .../cli/tests/command-runner-auth.test.ts | 2 +- packages/cli/tests/database.test.ts | 2 +- packages/cli/tests/helpers.ts | 9 +- packages/cli/tests/output.test.ts | 3 +- .../cli/tests/production-deploy-gate.test.ts | 3 +- packages/cli/tests/project-resolution.test.ts | 5 +- packages/cli/tests/project.test.ts | 2 +- packages/cli/tests/prompt.test.ts | 2 +- .../cli/tests/resolve-cli-version.test.ts | 2 +- packages/cli/tests/shell.test.ts | 2 +- packages/cli/tests/update-check.test.ts | 2 +- packages/cli/tests/use-case-helpers.ts | 4 +- packages/compute/src/index.ts | 2 +- scripts/smoke-cli-nextjs-artifact.mjs | 4 +- 68 files changed, 252 insertions(+), 274 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index df7bfd7..daac199 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -16,7 +16,7 @@ "linter": { "enabled": true, "rules": { - // "recommended": true, + "recommended": true // "complexity": { // "noExcessiveCognitiveComplexity": "on", // "noForEach": "on", diff --git a/packages/cli/src/adapters/mock-api.ts b/packages/cli/src/adapters/mock-api.ts index 43a69d4..5e5f3ff 100644 --- a/packages/cli/src/adapters/mock-api.ts +++ b/packages/cli/src/adapters/mock-api.ts @@ -320,10 +320,10 @@ export class MockApi { } 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 fb5d963..ffd4862 100644 --- a/packages/cli/src/adapters/token-storage.ts +++ b/packages/cli/src/adapters/token-storage.ts @@ -1,8 +1,8 @@ -import { CredentialsStore } from "@prisma/credentials-store"; -import type { TokenStorage, Tokens } from "@prisma/management-api-sdk"; 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 { diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index a05929c..9e9b829 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -22,9 +22,9 @@ import { } from "./shell/output"; import { disposePromptState } from "./shell/prompt"; import { + type CliRuntime, configureRuntimeCommand, createCommandContext, - type CliRuntime, } from "./shell/runtime"; import { createShellUi } from "./shell/ui"; import { maybeWriteCachedUpdateNotification } from "./shell/update-check"; diff --git a/packages/cli/src/commands/app/index.ts b/packages/cli/src/commands/app/index.ts index 078fe6c..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,19 +45,18 @@ import { serializeAppPromote, serializeAppRemove, serializeAppRollback, - serializeAppShow, serializeAppRun, + serializeAppShow, serializeAppShowDeploy, } from "../../presenters/app"; import { attachCommandDescriptor } from "../../shell/command-meta"; +import { runCommand, runStreamingCommand } from "../../shell/command-runner"; 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 { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime"; import type { AppBuildResult, AppDeployResult, @@ -69,9 +69,9 @@ import type { AppPromoteResult, AppRemoveResult, AppRollbackResult, - AppShowResult, AppRunResult, AppShowDeployResult, + AppShowResult, } from "../../types/app"; export function createAppCommand(runtime: CliRuntime): Command { diff --git a/packages/cli/src/commands/auth/index.ts b/packages/cli/src/commands/auth/index.ts index 887a4c8..9162b08 100644 --- a/packages/cli/src/commands/auth/index.ts +++ b/packages/cli/src/commands/auth/index.ts @@ -1,19 +1,19 @@ import { Command, Option } from "commander"; import { + type AuthLoginCommandOptions, runAuthLogin, runAuthLogout, runAuthWhoAmI, - type AuthLoginCommandOptions, } from "../../controllers/auth"; import { renderAuthSuccess } from "../../presenters/auth"; import { attachCommandDescriptor } from "../../shell/command-meta"; +import { runCommand } from "../../shell/command-runner"; import { addCompactGlobalFlags, addGlobalFlags, } from "../../shell/global-flags"; -import { runCommand } from "../../shell/command-runner"; -import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime"; +import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime"; import type { AuthStateResult } from "../../types/auth"; export function createAuthCommand(runtime: CliRuntime): Command { diff --git a/packages/cli/src/commands/branch/index.ts b/packages/cli/src/commands/branch/index.ts index 24b9fb5..fdf9621 100644 --- a/packages/cli/src/commands/branch/index.ts +++ b/packages/cli/src/commands/branch/index.ts @@ -3,12 +3,12 @@ import { Command } from "commander"; import { runBranchList } from "../../controllers/branch"; import { renderBranchList, serializeBranchList } from "../../presenters/branch"; import { attachCommandDescriptor } from "../../shell/command-meta"; +import { runCommand } from "../../shell/command-runner"; import { addCompactGlobalFlags, addGlobalFlags, } from "../../shell/global-flags"; -import { runCommand } from "../../shell/command-runner"; -import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime"; +import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime"; import type { BranchListResult } from "../../types/branch"; export function createBranchCommand(runtime: CliRuntime): Command { diff --git a/packages/cli/src/commands/database/index.ts b/packages/cli/src/commands/database/index.ts index 4d5cb6e..22efb4b 100644 --- a/packages/cli/src/commands/database/index.ts +++ b/packages/cli/src/commands/database/index.ts @@ -33,7 +33,7 @@ import { addCompactGlobalFlags, addGlobalFlags, } from "../../shell/global-flags"; -import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime"; +import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime"; import type { DatabaseConnectionCreateResult, DatabaseConnectionListResult, diff --git a/packages/cli/src/commands/env.ts b/packages/cli/src/commands/env.ts index c14dbe2..de14f37 100644 --- a/packages/cli/src/commands/env.ts +++ b/packages/cli/src/commands/env.ts @@ -19,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, diff --git a/packages/cli/src/commands/git/index.ts b/packages/cli/src/commands/git/index.ts index 1c8fc4b..8d396e3 100644 --- a/packages/cli/src/commands/git/index.ts +++ b/packages/cli/src/commands/git/index.ts @@ -5,13 +5,13 @@ import { renderGitConnect, renderGitDisconnect, } from "../../presenters/project"; -import { runCommand } from "../../shell/command-runner"; 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 { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime"; import type { ProjectRepositoryConnectionResult } from "../../types/project"; export function createGitCommand(runtime: CliRuntime): Command { diff --git a/packages/cli/src/commands/project/index.ts b/packages/cli/src/commands/project/index.ts index eb84df4..b0a96a2 100644 --- a/packages/cli/src/commands/project/index.ts +++ b/packages/cli/src/commands/project/index.ts @@ -7,20 +7,20 @@ import { runProjectShow, } from "../../controllers/project"; import { - renderProjectSetup, renderProjectList, + renderProjectSetup, renderProjectShow, - serializeProjectSetup, serializeProjectList, + serializeProjectSetup, serializeProjectShow, } from "../../presenters/project"; import { attachCommandDescriptor } from "../../shell/command-meta"; +import { runCommand } from "../../shell/command-runner"; import { addCompactGlobalFlags, addGlobalFlags, } from "../../shell/global-flags"; -import { runCommand } from "../../shell/command-runner"; -import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime"; +import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime"; import type { ProjectListResult, ProjectSetupResult, diff --git a/packages/cli/src/commands/version/index.ts b/packages/cli/src/commands/version/index.ts index fb5d8a7..c19c8e8 100644 --- a/packages/cli/src/commands/version/index.ts +++ b/packages/cli/src/commands/version/index.ts @@ -5,7 +5,7 @@ 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 { diff --git a/packages/cli/src/controllers/app-env-file.ts b/packages/cli/src/controllers/app-env-file.ts index b371ace..095bdd8 100644 --- a/packages/cli/src/controllers/app-env-file.ts +++ b/packages/cli/src/controllers/app-env-file.ts @@ -1,6 +1,6 @@ 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"; diff --git a/packages/cli/src/controllers/app-env.ts b/packages/cli/src/controllers/app-env.ts index 82a05c9..c947a18 100644 --- a/packages/cli/src/controllers/app-env.ts +++ b/packages/cli/src/controllers/app-env.ts @@ -1,18 +1,22 @@ 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, + readEnvFileAssignments, } from "../lib/app/env-file"; import { requireComputeAuth } from "../lib/auth/guard"; import { readLocalGitBranch } from "../lib/git/local-branch"; +import { + projectResolutionErrorToCliError, + resolveProjectTarget, +} from "../lib/project/resolution"; import { authRequiredError, CliError, @@ -21,16 +25,12 @@ import { } 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"; diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index 30e717c..9e8b0a0 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -1,12 +1,86 @@ 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 { + 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 { + DEFAULT_LOCAL_DEV_PORT, + resolveLocalBuildType, + runLocalApp, +} from "../lib/app/local-dev"; +import { + executePreviewBuild, + PREVIEW_BUILD_TYPES, + type PreviewBuildSettingsBuildType, + type PreviewBuildSettingsResolution, + type PreviewBuildType, + RESOLVED_PREVIEW_BUILD_TYPES, + type ResolvedPreviewBuildType, + resolveOrCreatePreviewBuildSettings, +} from "../lib/app/preview-build"; +import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction"; +import { + createPreviewDeployProgress, + createPreviewDeployProgressState, + createPreviewPromoteProgress, + type PreviewDeployProgressState, +} from "../lib/app/preview-progress"; +import { + createPreviewAppProvider, + type PreviewAppRecord, + PreviewDomainApiError, + type PreviewDomainRecord, +} from "../lib/app/preview-provider"; +import { enforceProductionDeployGate } from "../lib/app/production-deploy-gate"; +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, @@ -14,14 +88,14 @@ import { usageError, workspaceRequiredError, } from "../shell/errors"; -import { writeJsonEvent, type CommandSuccess } from "../shell/output"; -import { canPrompt, type CommandContext } from "../shell/runtime"; +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, - AppDeployResult, AppDeploymentSummary, + AppDeployResult, AppDomainAddResult, AppDomainDnsRecord, AppDomainRemoveResult, @@ -36,88 +110,13 @@ import type { AppRemoveResult, AppResolvedContext, AppRollbackResult, - AppShowResult, 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 { requireComputeAuth } from "../lib/auth/guard"; -import { readAuthState } from "../lib/auth/auth-ops"; -import { getApiBaseUrl, SERVICE_TOKEN_ENV_VAR } from "../lib/auth/client"; -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, -} 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, - createPreviewPromoteProgress, - type PreviewDeployProgressState, -} from "../lib/app/preview-progress"; -import { - createPreviewAppProvider, - PreviewDomainApiError, - type PreviewAppRecord, - type PreviewDomainRecord, -} from "../lib/app/preview-provider"; -import { enforceProductionDeployGate } from "../lib/app/production-deploy-gate"; -import { formatDomainFailureFix } from "../lib/app/domain-guidance"; import { requireAuthenticatedAuthState } from "./auth"; import { listRealWorkspaceProjects } from "./project"; import { createSelectPromptPort } from "./select-prompt-port"; diff --git a/packages/cli/src/controllers/auth.ts b/packages/cli/src/controllers/auth.ts index a3ed586..6d8aaa4 100644 --- a/packages/cli/src/controllers/auth.ts +++ b/packages/cli/src/controllers/auth.ts @@ -1,16 +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; diff --git a/packages/cli/src/controllers/branch.ts b/packages/cli/src/controllers/branch.ts index 09c2f50..ee85d0a 100644 --- a/packages/cli/src/controllers/branch.ts +++ b/packages/cli/src/controllers/branch.ts @@ -1,5 +1,9 @@ import type { ManagementApiClient } from "@prisma/management-api-sdk"; - +import { requireComputeAuth } from "../lib/auth/guard"; +import { + projectResolutionErrorToCliError, + resolveProjectTarget, +} from "../lib/project/resolution"; import { authRequiredError, CliError, @@ -12,13 +16,8 @@ import type { BranchRole, BranchSummary, } from "../types/branch"; -import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways"; 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"; diff --git a/packages/cli/src/controllers/database.ts b/packages/cli/src/controllers/database.ts index 2dcc62f..eebde1c 100644 --- a/packages/cli/src/controllers/database.ts +++ b/packages/cli/src/controllers/database.ts @@ -3,14 +3,14 @@ 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, + resolveProjectTarget, } from "../lib/project/resolution"; import { authRequiredError, diff --git a/packages/cli/src/controllers/project.ts b/packages/cli/src/controllers/project.ts index 2d39756..3253613 100644 --- a/packages/cli/src/controllers/project.ts +++ b/packages/cli/src/controllers/project.ts @@ -3,26 +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, @@ -33,7 +34,6 @@ import { resolveProjectForSetup, toProjectSummary, } from "../lib/project/setup"; -import { createPreviewAppProvider } from "../lib/app/preview-provider"; import { authRequiredError, CliError, @@ -42,7 +42,7 @@ import { 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 { diff --git a/packages/cli/src/lib/app/branch-database-deploy.ts b/packages/cli/src/lib/app/branch-database-deploy.ts index e9a1f84..fb88db6 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; diff --git a/packages/cli/src/lib/app/local-dev.ts b/packages/cli/src/lib/app/local-dev.ts index bec932b..2c0e624 100644 --- a/packages/cli/src/lib/app/local-dev.ts +++ b/packages/cli/src/lib/app/local-dev.ts @@ -1,16 +1,15 @@ -import { spawn, type SpawnOptions } from "node:child_process"; +import { type SpawnOptions, spawn } from "node:child_process"; import { access } from "node:fs/promises"; import path from "node:path"; - -import type { - PreviewBuildType, - ResolvedPreviewBuildType, -} from "./preview-build"; import { readBunPackageEntrypoint, readBunPackageJson, resolveBunEntrypoint, } from "./bun-project"; +import type { + PreviewBuildType, + ResolvedPreviewBuildType, +} from "./preview-build"; export type LocalBuildType = Extract< ResolvedPreviewBuildType, diff --git a/packages/cli/src/lib/app/preview-build-settings.ts b/packages/cli/src/lib/app/preview-build-settings.ts index 2b6d57d..9c687eb 100644 --- a/packages/cli/src/lib/app/preview-build-settings.ts +++ b/packages/cli/src/lib/app/preview-build-settings.ts @@ -2,10 +2,10 @@ import { exec } from "node:child_process"; import { readdir, readFile, stat, writeFile } from "node:fs/promises"; import path from "node:path"; -import { parseModule, type ASTNode } from "magicast"; +import { type ASTNode, parseModule } from "magicast"; import { CliError } from "../../shell/errors"; -import { readBunPackageJson, type BunPackageJsonLike } from "./bun-project"; +import { type BunPackageJsonLike, readBunPackageJson } from "./bun-project"; import type { ResolvedPreviewBuildType } from "./preview-build"; type PackageManager = "bun" | "pnpm" | "yarn" | "npm"; diff --git a/packages/cli/src/lib/app/preview-build.ts b/packages/cli/src/lib/app/preview-build.ts index 6eb2662..3348e67 100644 --- a/packages/cli/src/lib/app/preview-build.ts +++ b/packages/cli/src/lib/app/preview-build.ts @@ -17,32 +17,32 @@ import path from "node:path"; import { AstroBuild, - BunBuild, - NuxtBuild, type BuildArtifact, type BuildStrategy, + BunBuild, + NuxtBuild, } from "@prisma/compute-sdk"; import { readBunPackageJson, resolveBunEntrypoint } from "./bun-project"; import { - PRISMA_APP_CONFIG_FILENAME, hasAnyPackageDependency, hasPackageDependency, hasRootFile, joinPosix, nextOutputRootFromStandaloneDirectory, + PRISMA_APP_CONFIG_FILENAME, + type PreviewBuildSettings, resolvePreviewBuildSettings, runResolvedBuildCommand, - type PreviewBuildSettings, } from "./preview-build-settings"; export { PRISMA_APP_CONFIG_FILENAME, PRISMA_APP_CONFIG_SCHEMA_URL, - resolveOrCreatePreviewBuildSettings, - resolvePreviewBuildSettings, - type PreviewBuildSettingsBuildType, type PreviewBuildSettings, + type PreviewBuildSettingsBuildType, type PreviewBuildSettingsResolution, + resolveOrCreatePreviewBuildSettings, + resolvePreviewBuildSettings, } from "./preview-build-settings"; export const PREVIEW_BUILD_TYPES = [ diff --git a/packages/cli/src/lib/app/preview-progress.ts b/packages/cli/src/lib/app/preview-progress.ts index eeef44c..e2e7eb8 100644 --- a/packages/cli/src/lib/app/preview-progress.ts +++ b/packages/cli/src/lib/app/preview-progress.ts @@ -1,12 +1,11 @@ +import type { Writable } from "node:stream"; import type { DeployProgress, PromoteProgress, UpdateEnvProgress, } from "@prisma/compute-sdk"; -import type { Writable } from "node:stream"; - -import { renderDeployOutputRows } from "./deploy-output"; import type { ShellUi } from "../../shell/ui"; +import { renderDeployOutputRows } from "./deploy-output"; export interface PreviewDeployProgressState { buildStarted: boolean; diff --git a/packages/cli/src/lib/app/preview-provider.ts b/packages/cli/src/lib/app/preview-provider.ts index 94b1c78..d6cf95d 100644 --- a/packages/cli/src/lib/app/preview-provider.ts +++ b/packages/cli/src/lib/app/preview-provider.ts @@ -1,28 +1,26 @@ import path from "node:path"; - +import type { PortMapping, StreamRecord } from "@prisma/compute-sdk"; import { ApiError, CancelledError, ComputeClient, streamLogs, } from "@prisma/compute-sdk"; -import type { PortMapping, StreamRecord } from "@prisma/compute-sdk"; import type { ManagementApiClient } from "@prisma/management-api-sdk"; - -import { envVarNames } from "./env-vars"; -import { PreviewBuildStrategy } from "./preview-build"; -import type { PreviewBuildSettings, PreviewBuildType } from "./preview-build"; import type { BranchKind } from "../../types/branch"; +import { envVarNames } from "./env-vars"; import { createBranchDatabase, + createEnvironmentVariable, deleteBranchDatabase, deleteEnvironmentVariable, - createEnvironmentVariable, listEnvironmentVariables, - updateEnvironmentVariable, type PreviewBranchDatabaseRecord, type PreviewEnvironmentVariableRecord, + updateEnvironmentVariable, } from "./preview-branch-database"; +import type { PreviewBuildSettings, PreviewBuildType } from "./preview-build"; +import { PreviewBuildStrategy } from "./preview-build"; export interface PreviewAppRecord { id: string; diff --git a/packages/cli/src/lib/app/production-deploy-gate.ts b/packages/cli/src/lib/app/production-deploy-gate.ts index 6bcc703..4144d73 100644 --- a/packages/cli/src/lib/app/production-deploy-gate.ts +++ b/packages/cli/src/lib/app/production-deploy-gate.ts @@ -1,6 +1,6 @@ import { CliError } from "../../shell/errors"; import { confirmPrompt } from "../../shell/prompt"; -import { canPrompt, type CommandContext } from "../../shell/runtime"; +import { type CommandContext, canPrompt } from "../../shell/runtime"; import type { BranchKind } from "../../types/branch"; import type { PreviewAppProvider, diff --git a/packages/cli/src/lib/auth/login.ts b/packages/cli/src/lib/auth/login.ts index d1cd66a..8cb0ba1 100644 --- a/packages/cli/src/lib/auth/login.ts +++ b/packages/cli/src/lib/auth/login.ts @@ -7,13 +7,12 @@ import type { Readable, Writable } from "node:stream"; import { createManagementApiSdk, type ManagementApiSdk, - type TokenStorage, AuthError as SDKAuthError, + type TokenStorage, } from "@prisma/management-api-sdk"; import open from "open"; - -import { CLIENT_ID, getApiBaseUrl } from "./client"; import { FileTokenStorage } from "../../adapters/token-storage"; +import { CLIENT_ID, getApiBaseUrl } from "./client"; export class AuthError extends Error { constructor(message: string) { @@ -205,7 +204,6 @@ async function consumePastedCallback(options: { options.output.write( `Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`, ); - continue; } } } finally { diff --git a/packages/cli/src/lib/diagnostics.ts b/packages/cli/src/lib/diagnostics.ts index 92aa1f7..9a68372 100644 --- a/packages/cli/src/lib/diagnostics.ts +++ b/packages/cli/src/lib/diagnostics.ts @@ -1,5 +1,5 @@ import { resolveLocalStateFilePath } from "../adapters/local-state"; -import { resolveStateDir, type CommandContext } from "../shell/runtime"; +import { type CommandContext, resolveStateDir } from "../shell/runtime"; import type { CommandDiagnostics } from "../types/diagnostics"; import { readLocalGitState } from "./git/local-status"; diff --git a/packages/cli/src/lib/project/interactive-setup.ts b/packages/cli/src/lib/project/interactive-setup.ts index 9934d89..483b401 100644 --- a/packages/cli/src/lib/project/interactive-setup.ts +++ b/packages/cli/src/lib/project/interactive-setup.ts @@ -1,14 +1,14 @@ +import { usageError } from "../../shell/errors"; +import { selectPrompt, textPrompt } from "../../shell/prompt"; +import type { CommandContext } from "../../shell/runtime"; import type { ProjectSetupSuggestion, ProjectSummary, } from "../../types/project"; -import { usageError } from "../../shell/errors"; -import { selectPrompt, textPrompt } from "../../shell/prompt"; -import type { CommandContext } from "../../shell/runtime"; import { inferTargetName, - sortProjects, type ProjectCandidate, + sortProjects, } from "./resolution"; import { toProjectSummary, validateProjectSetupNameText } from "./setup"; diff --git a/packages/cli/src/lib/project/resolution.ts b/packages/cli/src/lib/project/resolution.ts index 66a3739..0db7d9e 100644 --- a/packages/cli/src/lib/project/resolution.ts +++ b/packages/cli/src/lib/project/resolution.ts @@ -2,10 +2,10 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import { + matchError, Result, TaggedError, - UnhandledException, - matchError, + type UnhandledException, } from "better-result"; import { formatCommandArgument } from "../../shell/command-arguments"; @@ -17,13 +17,13 @@ import type { BoundProjectShowResult, ProjectResolution, ProjectSetupSuggestion, + ProjectShowResult, ProjectSource, ProjectSummary, - ProjectShowResult, } from "../../types/project"; import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, - LocalResolutionPinReadAbortedError, + type LocalResolutionPinReadAbortedError, type LocalResolutionPinReadError, type LocalResolutionPinReadResult, readLocalResolutionPin, diff --git a/packages/cli/src/lib/project/setup.ts b/packages/cli/src/lib/project/setup.ts index bb99215..2ab4ed4 100644 --- a/packages/cli/src/lib/project/setup.ts +++ b/packages/cli/src/lib/project/setup.ts @@ -1,8 +1,8 @@ -import type { AuthWorkspace } from "../../types/auth"; -import type { ProjectSetupResult, ProjectSummary } from "../../types/project"; -import { Result, matchError } from "better-result"; +import { matchError, Result } from "better-result"; import { CliError, usageError } from "../../shell/errors"; import type { CommandContext } from "../../shell/runtime"; +import type { AuthWorkspace } from "../../types/auth"; +import type { ProjectSetupResult, ProjectSummary } from "../../types/project"; import { ensureLocalResolutionPinGitignore, LOCAL_RESOLUTION_PIN_RELATIVE_PATH, @@ -11,10 +11,11 @@ import { writeLocalResolutionPin, } from "./local-pin"; import { + type ProjectCandidate, projectAmbiguousError, projectNotFoundError, - type ProjectCandidate, } from "./resolution"; + export { formatCommandArgument } from "../../shell/command-arguments"; export type ProjectDirectoryBindingError = diff --git a/packages/cli/src/presenters/app-env.ts b/packages/cli/src/presenters/app-env.ts index 48dad82..2e2e525 100644 --- a/packages/cli/src/presenters/app-env.ts +++ b/packages/cli/src/presenters/app-env.ts @@ -1,15 +1,15 @@ +import { renderList, renderShow, serializeList } from "../output/patterns"; import type { CommandDescriptor } from "../shell/command-meta"; import type { CommandContext } from "../shell/runtime"; +import { renderVerboseBlock, type VerboseRow } from "../shell/ui"; import type { EnvAddResult, EnvListResult, - EnvRmResult, EnvResolvedContext, + EnvRmResult, EnvScopeDescriptor, EnvUpdateResult, } from "../types/app-env"; -import { renderList, renderShow, serializeList } from "../output/patterns"; -import { renderVerboseBlock, type VerboseRow } from "../shell/ui"; import { renderResolvedProjectContextBlock, stripVerboseContext, diff --git a/packages/cli/src/presenters/app.ts b/packages/cli/src/presenters/app.ts index 7f0e286..f71e0b0 100644 --- a/packages/cli/src/presenters/app.ts +++ b/packages/cli/src/presenters/app.ts @@ -1,9 +1,13 @@ +import { renderDeployOutputRows } from "../lib/app/deploy-output"; +import { formatDomainFailureFix } from "../lib/app/domain-guidance"; +import { renderList, renderShow, serializeList } from "../output/patterns"; import type { CommandDescriptor } from "../shell/command-meta"; import type { CommandContext } from "../shell/runtime"; +import { renderVerboseBlock, type VerboseRow } from "../shell/ui"; import type { AppBuildResult, - AppDeploySettings, AppDeployResult, + AppDeploySettings, AppDomainAddResult, AppDomainRemoveResult, AppDomainRetryResult, @@ -14,14 +18,10 @@ import type { AppPromoteResult, AppRemoveResult, AppRollbackResult, - AppShowResult, AppRunResult, AppShowDeployResult, + AppShowResult, } from "../types/app"; -import { renderList, renderShow, serializeList } from "../output/patterns"; -import { renderDeployOutputRows } from "../lib/app/deploy-output"; -import { formatDomainFailureFix } from "../lib/app/domain-guidance"; -import { renderVerboseBlock, type VerboseRow } from "../shell/ui"; import { renderResolvedProjectContextBlock, stripVerboseContext, diff --git a/packages/cli/src/presenters/auth.ts b/packages/cli/src/presenters/auth.ts index 8fd3ee0..cd9e861 100644 --- a/packages/cli/src/presenters/auth.ts +++ b/packages/cli/src/presenters/auth.ts @@ -1,7 +1,7 @@ +import { renderMutate, renderShow } from "../output/patterns"; import type { CommandDescriptor } from "../shell/command-meta"; import type { CommandContext } from "../shell/runtime"; import type { AuthProviderId, AuthStateResult } from "../types/auth"; -import { renderMutate, renderShow } from "../output/patterns"; export function renderAuthSuccess( context: CommandContext, diff --git a/packages/cli/src/presenters/database.ts b/packages/cli/src/presenters/database.ts index f1b1b92..fb6c18d 100644 --- a/packages/cli/src/presenters/database.ts +++ b/packages/cli/src/presenters/database.ts @@ -1,8 +1,8 @@ +import { renderMutate, renderShow, serializeList } from "../output/patterns"; import type { CommandDescriptor } from "../shell/command-meta"; import { formatDescriptorLabel } from "../shell/command-meta"; import type { CommandContext } from "../shell/runtime"; import { formatColumns, renderSummaryLine } from "../shell/ui"; -import { renderMutate, renderShow, serializeList } from "../output/patterns"; import type { DatabaseConnectionCreateResult, DatabaseConnectionListResult, diff --git a/packages/cli/src/presenters/project.ts b/packages/cli/src/presenters/project.ts index 14ccf8f..e0776c6 100644 --- a/packages/cli/src/presenters/project.ts +++ b/packages/cli/src/presenters/project.ts @@ -1,11 +1,17 @@ import path from "node:path"; import stringWidth from "string-width"; - +import { renderMutate, renderShow, serializeList } from "../output/patterns"; import { formatCommandArgument } from "../shell/command-arguments"; import type { CommandDescriptor } from "../shell/command-meta"; import { formatDescriptorLabel } from "../shell/command-meta"; import type { CommandContext } from "../shell/runtime"; +import { + padDisplay, + renderNextSteps, + renderSummaryLine, + renderVerboseBlock, +} from "../shell/ui"; import type { GitRepositoryConnection, ProjectListResult, @@ -13,13 +19,6 @@ import type { ProjectSetupResult, ProjectShowResult, } from "../types/project"; -import { renderMutate, renderShow, serializeList } from "../output/patterns"; -import { - padDisplay, - renderNextSteps, - renderSummaryLine, - renderVerboseBlock, -} from "../shell/ui"; import { renderResolvedProjectContextBlock } from "./verbose-context"; export function renderProjectList( diff --git a/packages/cli/src/shell/command-runner.ts b/packages/cli/src/shell/command-runner.ts index e16ddbf..36317fb 100644 --- a/packages/cli/src/shell/command-runner.ts +++ b/packages/cli/src/shell/command-runner.ts @@ -1,7 +1,7 @@ import { AuthError as SDKAuthError } from "@prisma/management-api-sdk"; +import { collectCommandDiagnostics } from "../lib/diagnostics"; import type { CommandDescriptor } from "./command-meta"; import { getCommandDescriptor } from "./command-meta"; -import { collectCommandDiagnostics } from "../lib/diagnostics"; import { renderCommandDiagnostics } from "./diagnostics-output"; import { authRequiredError, CliError, commandCanceledError } from "./errors"; import { resolveGlobalFlags } from "./global-flags"; @@ -14,7 +14,7 @@ import { writeJsonEvent, writeJsonSuccess, } from "./output"; -import { createCommandContext, type CliRuntime } from "./runtime"; +import { type CliRuntime, createCommandContext } from "./runtime"; interface CommandPresenter { renderStdout?: ( diff --git a/packages/cli/src/shell/global-flags.ts b/packages/cli/src/shell/global-flags.ts index 45a09f2..5c91a52 100644 --- a/packages/cli/src/shell/global-flags.ts +++ b/packages/cli/src/shell/global-flags.ts @@ -1,4 +1,4 @@ -import { Command, Option } from "commander"; +import { type Command, Option } from "commander"; export interface GlobalFlags { json: boolean; diff --git a/packages/cli/src/shell/help.ts b/packages/cli/src/shell/help.ts index e230857..7f69aec 100644 --- a/packages/cli/src/shell/help.ts +++ b/packages/cli/src/shell/help.ts @@ -1,6 +1,6 @@ import type { Argument, Command, Option } from "commander"; -import { getDescriptorForCommand, formatDescriptorLabel } from "./command-meta"; +import { formatDescriptorLabel, getDescriptorForCommand } from "./command-meta"; import { COMPACT_GLOBAL_OPTION_FLAGS, resolveGlobalFlags, diff --git a/packages/cli/src/shell/prompt.ts b/packages/cli/src/shell/prompt.ts index 45c975c..66192d7 100644 --- a/packages/cli/src/shell/prompt.ts +++ b/packages/cli/src/shell/prompt.ts @@ -1,5 +1,5 @@ -import { confirm, isCancel, select, text } from "@clack/prompts"; import type { Readable, Writable } from "node:stream"; +import { confirm, isCancel, select, text } from "@clack/prompts"; import { usageError } from "./errors"; diff --git a/packages/cli/src/shell/runtime.ts b/packages/cli/src/shell/runtime.ts index aef53f7..8b26d1e 100644 --- a/packages/cli/src/shell/runtime.ts +++ b/packages/cli/src/shell/runtime.ts @@ -1,12 +1,12 @@ import path from "node:path"; -import { Command } from "commander"; +import type { Command } from "commander"; import { LocalStateStore } from "../adapters/local-state"; import { MockApi } from "../adapters/mock-api"; +import type { GlobalFlags } from "./global-flags"; import { renderHelp } from "./help"; import type { CliOutput } from "./output"; -import type { GlobalFlags } from "./global-flags"; import { createShellUi, type ShellUi } from "./ui"; export const DEFAULT_STATE_DIR_NAME = path.join(".prisma", "cli"); diff --git a/packages/cli/src/shell/ui.ts b/packages/cli/src/shell/ui.ts index 8c249a9..8aa50d3 100644 --- a/packages/cli/src/shell/ui.ts +++ b/packages/cli/src/shell/ui.ts @@ -1,7 +1,7 @@ +import { createColors } from "colorette"; import stringWidth from "string-width"; import stripAnsi from "strip-ansi"; import wrapAnsi from "wrap-ansi"; -import { createColors } from "colorette"; import type { GlobalFlags } from "./global-flags"; import type { CliRuntime } from "./runtime"; diff --git a/packages/cli/src/shell/update-check.ts b/packages/cli/src/shell/update-check.ts index 833a5cd..63efee9 100644 --- a/packages/cli/src/shell/update-check.ts +++ b/packages/cli/src/shell/update-check.ts @@ -1,8 +1,8 @@ import { spawn } from "node:child_process"; +import { randomUUID } from "node:crypto"; import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { randomUUID } from "node:crypto"; import { getCliName, getCliVersion } from "../lib/version"; import type { CliRuntime } from "./runtime"; diff --git a/packages/cli/src/use-cases/branch.ts b/packages/cli/src/use-cases/branch.ts index 88e5633..0f4d76d 100644 --- a/packages/cli/src/use-cases/branch.ts +++ b/packages/cli/src/use-cases/branch.ts @@ -1,7 +1,7 @@ -import type { BranchSummary, BranchListResult } from "../types/branch"; +import type { BranchListResult, BranchSummary } from "../types/branch"; import type { - BranchUseCases, BranchGateway, + BranchUseCases, ProjectGateway, ProjectStateGateway, RemoteBranchRecord, diff --git a/packages/cli/tests/app-bun-compat.test.ts b/packages/cli/tests/app-bun-compat.test.ts index 7b7df08..02dcd53 100644 --- a/packages/cli/tests/app-bun-compat.test.ts +++ b/packages/cli/tests/app-bun-compat.test.ts @@ -17,9 +17,9 @@ function mockBuildStrategy( execute: vi.fn(), }), ) { - return vi.fn().mockImplementation(function (options: object) { - return createInstance(options); - }); + return vi + .fn() + .mockImplementation((options: object) => createInstance(options)); } describe("bun compatibility", () => { diff --git a/packages/cli/tests/app-env-presenter.test.ts b/packages/cli/tests/app-env-presenter.test.ts index f482fe4..026b45a 100644 --- a/packages/cli/tests/app-env-presenter.test.ts +++ b/packages/cli/tests/app-env-presenter.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest"; import stripAnsi from "strip-ansi"; +import { describe, expect, it } from "vitest"; import { renderEnvList, serializeEnvList } from "../src/presenters/app-env"; import { getCommandDescriptor } from "../src/shell/command-meta"; diff --git a/packages/cli/tests/app-env-vars.test.ts b/packages/cli/tests/app-env-vars.test.ts index c21e29e..90c43c8 100644 --- a/packages/cli/tests/app-env-vars.test.ts +++ b/packages/cli/tests/app-env-vars.test.ts @@ -1,5 +1,5 @@ -import path from "node:path"; import { writeFile } from "node:fs/promises"; +import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/packages/cli/tests/app-presenter.test.ts b/packages/cli/tests/app-presenter.test.ts index 2109e93..503e84b 100644 --- a/packages/cli/tests/app-presenter.test.ts +++ b/packages/cli/tests/app-presenter.test.ts @@ -1,6 +1,4 @@ import { describe, expect, it } from "vitest"; - -import { getCommandDescriptor } from "../src/shell/command-meta"; import { renderAppDeploy, renderAppDomainAdd, @@ -8,6 +6,7 @@ import { renderAppDomainShow, serializeAppDeploy, } from "../src/presenters/app"; +import { getCommandDescriptor } from "../src/shell/command-meta"; import type { AppDeployResult, AppDomainAddResult, diff --git a/packages/cli/tests/app-provider.test.ts b/packages/cli/tests/app-provider.test.ts index ea29f7b..14e1a73 100644 --- a/packages/cli/tests/app-provider.test.ts +++ b/packages/cli/tests/app-provider.test.ts @@ -10,9 +10,7 @@ afterEach(() => { }); function mockPreviewBuildStrategy() { - return vi.fn().mockImplementation(function (options: object) { - return { options }; - }); + return vi.fn().mockImplementation((options: object) => ({ options })); } describe("preview app provider", () => { diff --git a/packages/cli/tests/app-state.test.ts b/packages/cli/tests/app-state.test.ts index 2441417..24e3d2c 100644 --- a/packages/cli/tests/app-state.test.ts +++ b/packages/cli/tests/app-state.test.ts @@ -1,5 +1,5 @@ -import path from "node:path"; import { readFile } from "node:fs/promises"; +import path from "node:path"; import { describe, expect, it } from "vitest"; diff --git a/packages/cli/tests/auth-login.test.ts b/packages/cli/tests/auth-login.test.ts index 8187497..398c5f6 100644 --- a/packages/cli/tests/auth-login.test.ts +++ b/packages/cli/tests/auth-login.test.ts @@ -1,8 +1,6 @@ import { PassThrough } from "node:stream"; - -import { afterEach, describe, expect, it, vi } from "vitest"; - import type { TokenStorage } from "@prisma/management-api-sdk"; +import { afterEach, describe, expect, it, vi } from "vitest"; afterEach(() => { vi.doUnmock("@prisma/management-api-sdk"); diff --git a/packages/cli/tests/auth-ops.test.ts b/packages/cli/tests/auth-ops.test.ts index 69f667d..f353e55 100644 --- a/packages/cli/tests/auth-ops.test.ts +++ b/packages/cli/tests/auth-ops.test.ts @@ -15,9 +15,7 @@ function encodeJwt(claims: Record): string { } function mockFileTokenStorage(getTokens: ReturnType) { - return vi.fn().mockImplementation(function () { - return { getTokens }; - }); + return vi.fn().mockImplementation(() => ({ getTokens })); } describe("readAuthState", () => { diff --git a/packages/cli/tests/auth.test.ts b/packages/cli/tests/auth.test.ts index bd0e4d1..23e5339 100644 --- a/packages/cli/tests/auth.test.ts +++ b/packages/cli/tests/auth.test.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import { describe, expect, it } from "vitest"; import stripAnsi from "strip-ansi"; +import { describe, expect, it } from "vitest"; import { createTempCwd, executeCli } from "./helpers"; diff --git a/packages/cli/tests/branch.test.ts b/packages/cli/tests/branch.test.ts index 6e01f15..df9f2eb 100644 --- a/packages/cli/tests/branch.test.ts +++ b/packages/cli/tests/branch.test.ts @@ -1,11 +1,9 @@ import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; - -import { describe, expect, it } from "vitest"; import stripAnsi from "strip-ansi"; - -import { getCommandDescriptor } from "../src/shell/command-meta"; +import { describe, expect, it } from "vitest"; import { renderBranchList } from "../src/presenters/branch"; +import { getCommandDescriptor } from "../src/shell/command-meta"; import { createTempCwd, createTestCommandContext, executeCli } from "./helpers"; const fixturePath = path.resolve("fixtures/mock-api.json"); diff --git a/packages/cli/tests/command-runner-auth.test.ts b/packages/cli/tests/command-runner-auth.test.ts index b971880..708fa84 100644 --- a/packages/cli/tests/command-runner-auth.test.ts +++ b/packages/cli/tests/command-runner-auth.test.ts @@ -1,5 +1,5 @@ -import { AuthError as SDKAuthError } from "@prisma/management-api-sdk"; import { PassThrough, Writable } from "node:stream"; +import { AuthError as SDKAuthError } from "@prisma/management-api-sdk"; import { afterEach, describe, expect, it } from "vitest"; import { runCommand, runStreamingCommand } from "../src/shell/command-runner"; diff --git a/packages/cli/tests/database.test.ts b/packages/cli/tests/database.test.ts index 3244121..49798d9 100644 --- a/packages/cli/tests/database.test.ts +++ b/packages/cli/tests/database.test.ts @@ -1,7 +1,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; -import { describe, expect, it } from "vitest"; import stripAnsi from "strip-ansi"; +import { describe, expect, it } from "vitest"; import { createTempCwd, executeCli } from "./helpers"; diff --git a/packages/cli/tests/helpers.ts b/packages/cli/tests/helpers.ts index 85ec21c..0ca5322 100644 --- a/packages/cli/tests/helpers.ts +++ b/packages/cli/tests/helpers.ts @@ -2,16 +2,15 @@ import { mkdtemp, readFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { PassThrough, Writable } from "node:stream"; - -import { runCli } from "../src/cli"; import { LocalStateStore } from "../src/adapters/local-state"; +import { runCli } from "../src/cli"; +import type { GlobalFlags } from "../src/shell/global-flags"; import { - createCommandContext, - resolveStateDir, type CliRuntime, type CommandContext, + createCommandContext, + resolveStateDir, } from "../src/shell/runtime"; -import type { GlobalFlags } from "../src/shell/global-flags"; class CaptureStream extends Writable { buffer = ""; diff --git a/packages/cli/tests/output.test.ts b/packages/cli/tests/output.test.ts index 34644cf..60e31b4 100644 --- a/packages/cli/tests/output.test.ts +++ b/packages/cli/tests/output.test.ts @@ -1,8 +1,7 @@ import { describe, expect, it } from "vitest"; - +import { renderDeployOutputRows } from "../src/lib/app/deploy-output"; import { CliError } from "../src/shell/errors"; import { writeHumanError } from "../src/shell/output"; -import { renderDeployOutputRows } from "../src/lib/app/deploy-output"; import { plain } from "../src/shell/ui"; import { createTempCwd, createTestCommandContext } from "./helpers"; diff --git a/packages/cli/tests/production-deploy-gate.test.ts b/packages/cli/tests/production-deploy-gate.test.ts index ba8e1ba..b68b032 100644 --- a/packages/cli/tests/production-deploy-gate.test.ts +++ b/packages/cli/tests/production-deploy-gate.test.ts @@ -1,13 +1,12 @@ import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; - -import { confirmPrompt } from "../src/shell/prompt"; import type { PreviewAppProvider, PreviewAppRecord, PreviewDeploymentRecord, } from "../src/lib/app/preview-provider"; +import { confirmPrompt } from "../src/shell/prompt"; import { createTempCwd, createTestCommandContext } from "./helpers"; vi.mock("../src/shell/prompt", async () => { diff --git a/packages/cli/tests/project-resolution.test.ts b/packages/cli/tests/project-resolution.test.ts index 9e3bb35..d6282c0 100644 --- a/packages/cli/tests/project-resolution.test.ts +++ b/packages/cli/tests/project-resolution.test.ts @@ -2,13 +2,12 @@ import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; import type { Result } from "better-result"; import { describe, expect, it, vi } from "vitest"; - -import { createTempCwd, createTestCommandContext } from "./helpers"; +import type { ProjectCandidate } from "../src/lib/project/resolution"; import { projectResolutionErrorToCliError, resolveProjectTarget, } from "../src/lib/project/resolution"; -import type { ProjectCandidate } from "../src/lib/project/resolution"; +import { createTempCwd, createTestCommandContext } from "./helpers"; async function writeLocalPin(cwd: string, pin: unknown) { await mkdir(path.join(cwd, ".prisma"), { recursive: true }); diff --git a/packages/cli/tests/project.test.ts b/packages/cli/tests/project.test.ts index 270c7ba..186c427 100644 --- a/packages/cli/tests/project.test.ts +++ b/packages/cli/tests/project.test.ts @@ -1,7 +1,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; -import { describe, expect, it } from "vitest"; import stripAnsi from "strip-ansi"; +import { describe, expect, it } from "vitest"; import { createTempCwd, executeCli } from "./helpers"; diff --git a/packages/cli/tests/prompt.test.ts b/packages/cli/tests/prompt.test.ts index 93080ee..8b59b40 100644 --- a/packages/cli/tests/prompt.test.ts +++ b/packages/cli/tests/prompt.test.ts @@ -1,5 +1,5 @@ -import { setImmediate as nextTick } from "node:timers/promises"; import { PassThrough } from "node:stream"; +import { setImmediate as nextTick } from "node:timers/promises"; import { describe, expect, it } from "vitest"; diff --git a/packages/cli/tests/resolve-cli-version.test.ts b/packages/cli/tests/resolve-cli-version.test.ts index f9c1452..d7061f0 100644 --- a/packages/cli/tests/resolve-cli-version.test.ts +++ b/packages/cli/tests/resolve-cli-version.test.ts @@ -1,7 +1,7 @@ import { execFile } from "node:child_process"; -import { promisify } from "node:util"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; diff --git a/packages/cli/tests/shell.test.ts b/packages/cli/tests/shell.test.ts index 228e3cf..351289b 100644 --- a/packages/cli/tests/shell.test.ts +++ b/packages/cli/tests/shell.test.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import { describe, expect, it } from "vitest"; import stripAnsi from "strip-ansi"; +import { describe, expect, it } from "vitest"; import { formatCommandArgument } from "../src/shell/command-arguments"; import { formatUnexpectedError } from "../src/shell/output"; diff --git a/packages/cli/tests/update-check.test.ts b/packages/cli/tests/update-check.test.ts index 058aa44..9654c6d 100644 --- a/packages/cli/tests/update-check.test.ts +++ b/packages/cli/tests/update-check.test.ts @@ -1,5 +1,5 @@ -import path from "node:path"; import { access, mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; import { describe, expect, it } from "vitest"; import { getCliVersion } from "../src/lib/version"; diff --git a/packages/cli/tests/use-case-helpers.ts b/packages/cli/tests/use-case-helpers.ts index be241ea..6388a7e 100644 --- a/packages/cli/tests/use-case-helpers.ts +++ b/packages/cli/tests/use-case-helpers.ts @@ -1,8 +1,8 @@ import path from "node:path"; import { MockApi } from "../src/adapters/mock-api"; -import type { CliUseCaseGateways } from "../src/use-cases/create-cli-gateways"; import type { AuthSessionRecord } from "../src/use-cases/contracts"; +import type { CliUseCaseGateways } from "../src/use-cases/create-cli-gateways"; const fixturePath = path.resolve("fixtures/mock-api.json"); @@ -18,7 +18,7 @@ export async function createUseCaseGateways(options?: { }> { const api = await MockApi.load(fixturePath); let authSession = options?.authSession ?? null; - let projectId = options?.projectId ?? null; + const projectId = options?.projectId ?? null; return { gateways: { diff --git a/packages/compute/src/index.ts b/packages/compute/src/index.ts index 3a3dd4c..c4c4af0 100644 --- a/packages/compute/src/index.ts +++ b/packages/compute/src/index.ts @@ -1,5 +1,5 @@ export { ScaleToZeroGuard, - waitUntil, type ScaleToZeroGuardOptions, + waitUntil, } from "./scale-to-zero"; diff --git a/scripts/smoke-cli-nextjs-artifact.mjs b/scripts/smoke-cli-nextjs-artifact.mjs index 2bb604e..15856fd 100644 --- a/scripts/smoke-cli-nextjs-artifact.mjs +++ b/scripts/smoke-cli-nextjs-artifact.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node -import { createRequire } from "node:module"; -import { rm } from "node:fs/promises"; import { spawn } from "node:child_process"; +import { rm } from "node:fs/promises"; +import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; From 9f8dc0ce6fc43f559cf9c3c08db757b4c9726829 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:37:52 -0400 Subject: [PATCH 03/28] enable cognitive complexity lint rule --- biome.jsonc | 6 +- packages/cli/src/adapters/token-storage.ts | 2 +- packages/cli/src/controllers/app-env.ts | 13 +- packages/cli/src/controllers/app.ts | 487 ++++++++++-------- packages/cli/src/controllers/project.ts | 38 +- .../cli/src/lib/app/branch-database-deploy.ts | 211 +++++--- packages/cli/src/lib/app/branch-database.ts | 102 ++-- packages/cli/src/lib/app/preview-build.ts | 71 ++- packages/cli/src/lib/app/preview-provider.ts | 73 +-- packages/cli/src/lib/auth/login.ts | 76 +-- packages/cli/src/lib/project/setup.ts | 5 +- packages/cli/src/presenters/app.ts | 2 +- packages/cli/src/shell/command-runner.ts | 111 ++-- packages/cli/src/shell/help.ts | 114 ++-- packages/cli/tests/app-env-vars.test.ts | 4 +- packages/cli/tests/project-real-mode.test.ts | 219 +++----- packages/compute/tests/scale-to-zero.test.ts | 5 +- 17 files changed, 876 insertions(+), 663 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index daac199..91b68c4 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -16,9 +16,11 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "complexity": { + "noExcessiveCognitiveComplexity": "on" + } // "complexity": { - // "noExcessiveCognitiveComplexity": "on", // "noForEach": "on", // "noImplicitCoercions": "on" // }, diff --git a/packages/cli/src/adapters/token-storage.ts b/packages/cli/src/adapters/token-storage.ts index ffd4862..8e0b36b 100644 --- a/packages/cli/src/adapters/token-storage.ts +++ b/packages/cli/src/adapters/token-storage.ts @@ -81,7 +81,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; } diff --git a/packages/cli/src/controllers/app-env.ts b/packages/cli/src/controllers/app-env.ts index c947a18..9a79543 100644 --- a/packages/cli/src/controllers/app-env.ts +++ b/packages/cli/src/controllers/app-env.ts @@ -528,20 +528,17 @@ async function requireClientAndProject( 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, - ), + listRealWorkspaceProjects(client, workspace, context.runtime.signal), commandName, }); if (targetResult.isErr()) { @@ -553,7 +550,7 @@ async function requireClientAndProject( client, projectId: target.project.id, verboseContext: { - workspace: authState.workspace, + workspace, project: target.project, resolution: target.resolution, }, diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index 9e8b0a0..5052f0c 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -33,7 +33,6 @@ import { type PreviewBuildSettingsResolution, type PreviewBuildType, RESOLVED_PREVIEW_BUILD_TYPES, - type ResolvedPreviewBuildType, resolveOrCreatePreviewBuildSettings, } from "../lib/app/preview-build"; import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction"; @@ -1884,87 +1883,122 @@ function domainCommandError( hostname: string, ): CliError { if (error instanceof PreviewDomainApiError) { - if ( - command === "add" && - (error.status === 400 || error.status === 422) && - isDomainDnsError(error) - ) { - return domainDnsNotConfiguredError(hostname, error); - } + return domainApiCommandError(command, error, hostname); + } - 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"], - }); - } + 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}`], + }); +} - 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 "], - }); - } +function domainApiCommandError( + command: AppDomainCommand, + error: PreviewDomainApiError, + hostname: string, +): CliError { + if (command === "add") { + return domainAddCommandError(error, hostname); + } - if (command === "add" && error.status === 409) { - return domainAlreadyRegisteredError(hostname, error); - } + if (error.status === 404) { + return domainNotFoundError(hostname); + } - 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 === "retry" && error.status === 409) { + return domainRetryNotEligibleError(error, hostname); + } - if ( - (command === "show" || - command === "remove" || - command === "retry" || - command === "wait") && - error.status === 404 - ) { - return domainNotFoundError(hostname); - } + return domainGenericCommandError(command, error, 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}`], - }); - } +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 domainGenericCommandError( + command: AppDomainCommand, + error: unknown, + hostname: string, +): CliError { return new CliError({ code: "DEPLOY_FAILED", domain: "app", @@ -2793,21 +2827,18 @@ 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, - ), + listRealWorkspaceProjects(client, workspace, context.runtime.signal), commandName: options?.commandName, }); if (resolvedResult.isErr()) { @@ -2853,27 +2884,90 @@ async function resolveDeployProjectContext( 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, - { - workspace, - project: toProjectSummary(project), - resolution: { - projectSource: "explicit", - targetName: explicitProject, - targetNameSource: "explicit", - }, - localPinAction: "linked", + return { + workspace, + project: toProjectSummary(project), + resolution: { + projectSource: "explicit", + targetName: explicitProject, + targetNameSource: "explicit", }, - branch, - context.runtime.signal, - ); + localPinAction: "linked", + }; } if (options.createProjectName) { @@ -2888,21 +2982,16 @@ async function resolveDeployProjectContext( workspace, context.runtime.signal, ); - return withRemoteDeployBranch( - provider, - { - workspace, - project: toProjectSummary(created), - resolution: { - projectSource: "created", - targetName: projectName, - targetNameSource: "explicit", - }, - localPinAction: "created", + return { + workspace, + project: toProjectSummary(created), + resolution: { + projectSource: "created", + targetName: projectName, + targetNameSource: "explicit", }, - branch, - context.runtime.signal, - ); + localPinAction: "created", + }; } if (options.envProjectId) { @@ -2912,20 +3001,15 @@ async function resolveDeployProjectContext( if (!project) { throw projectNotFoundError(options.envProjectId, workspace); } - return withRemoteDeployBranch( - provider, - { - workspace, - project: toProjectSummary(project), - resolution: { - projectSource: "env", - targetName: options.envProjectId, - targetNameSource: "env", - }, + return { + workspace, + project: toProjectSummary(project), + resolution: { + projectSource: "env", + targetName: options.envProjectId, + targetNameSource: "env", }, - branch, - context.runtime.signal, - ); + }; } const localPin = options.localPin; @@ -2941,60 +3025,31 @@ async function resolveDeployProjectContext( throw localResolutionPinStaleError(); } - return withRemoteDeployBranch( - provider, - { - workspace, - project: toProjectSummary(project), - resolution: { - projectSource: "local-pin", - targetName: project.name, - targetNameSource: "local-pin", - }, + return { + workspace, + project: toProjectSummary(project), + resolution: { + projectSource: "local-pin", + targetName: project.name, + targetNameSource: "local-pin", }, - branch, - context.runtime.signal, - ); + }; } const platformMapping = await resolveDurablePlatformMapping(); if (platformMapping && platformMapping.workspace.id === workspace.id) { - return withRemoteDeployBranch( - provider, - { - workspace, - project: toProjectSummary(platformMapping), - resolution: { - projectSource: "platform-mapping", - targetName: platformMapping.name, - targetNameSource: "platform-mapping", - }, - }, - branch, - context.runtime.signal, - ); - } - - if (canPrompt(context) && !context.flags.yes) { - const resolved = await resolveInteractiveDeployProjectSetup( - context, - provider, + return { workspace, - projects, - ); - return withRemoteDeployBranch( - provider, - resolved, - branch, - context.runtime.signal, - ); + project: toProjectSummary(platformMapping), + resolution: { + projectSource: "platform-mapping", + targetName: platformMapping.name, + targetNameSource: "platform-mapping", + }, + }; } - const suggestedName = await inferTargetName( - context.runtime.cwd, - context.runtime.signal, - ); - throw projectSetupRequiredError(projects, suggestedName); + return null; } async function resolveInteractiveDeployProjectSetup( @@ -3894,52 +3949,7 @@ function appDeployFailedError( 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) { @@ -4006,6 +4016,55 @@ function appDeployFailedError( }); } +function appBuildFailedError(why: string, debug: string | 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", diff --git a/packages/cli/src/controllers/project.ts b/packages/cli/src/controllers/project.ts index 3253613..cc6135e 100644 --- a/packages/cli/src/controllers/project.ts +++ b/packages/cli/src/controllers/project.ts @@ -936,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: { @@ -1001,25 +1020,6 @@ interface SourceRepositoryApiClient { }; }> >; - 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; - }; - }> - >; DELETE( path: "/v1/source-repositories/{id}", options: { diff --git a/packages/cli/src/lib/app/branch-database-deploy.ts b/packages/cli/src/lib/app/branch-database-deploy.ts index fb88db6..a1f4394 100644 --- a/packages/cli/src/lib/app/branch-database-deploy.ts +++ b/packages/cli/src/lib/app/branch-database-deploy.ts @@ -53,29 +53,9 @@ export async function maybeSetupBranchDatabase( return emptyBranchDatabaseSetupOutcome(); } - if (hasProvidedDatabaseEnvVars(options.providedEnvVars)) { - if (options.db === true) { - throw usageError( - "Database setup cannot be combined with provided database env vars", - "The deploy command received --db and a DATABASE_URL or DIRECT_URL value from --env.", - "Remove the --env database value to let --db create and wire a database, or remove --db to deploy with the provided value.", - [ - "prisma-cli app deploy --db", - "prisma-cli app deploy --env DATABASE_URL=postgresql://example", - ], - "app", - ); - } - - return emptyBranchDatabaseSetupOutcome(); - } - - if (branch.kind === "production" && !options.firstProductionDeploy) { - if (options.db === true) { - throw productionDatabaseSetupAfterFirstDeployError(); - } - - return emptyBranchDatabaseSetupOutcome(); + const preflight = branchDatabasePreflight(branch, options); + if (preflight) { + return preflight; } const envState = await inspectBranchDatabaseEnv( @@ -86,27 +66,15 @@ export async function maybeSetupBranchDatabase( ); const targetEnvVars = getTargetDatabaseEnvVarKeys(envState); - if (hasExistingDatabaseEnvForTarget(branch, envState)) { - const warning = - options.db === true - ? existingDatabaseEnvWarning(branch, targetEnvVars) - : null; - if (warning) { - emitBranchDatabaseWarning(context, warning); - } - - return { - result: - options.db === true - ? { - status: "skipped", - reason: existingDatabaseEnvReason(branch), - envVars: targetEnvVars, - schema: null, - } - : undefined, - warnings: warning ? [warning] : [], - }; + const existingEnvOutcome = existingBranchDatabaseEnvOutcome( + context, + branch, + targetEnvVars, + envState, + options.db, + ); + if (existingEnvOutcome) { + return existingEnvOutcome; } const localSignal = await inspectBranchDatabaseSignal( @@ -125,35 +93,18 @@ export async function maybeSetupBranchDatabase( 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, - }); + const promptOutcome = await branchDatabasePromptOutcome( + context, + branch, + localSignal, + envState, + options.db, + ); + if (promptOutcome) { + return promptOutcome; + } - if (!shouldCreate) { - return emptyBranchDatabaseSetupOutcome(); - } - } else if (!canPrompt(context) && !context.flags.yes) { + if (options.db === true && !canPrompt(context) && !context.flags.yes) { throw nonInteractiveDatabaseSetupRequiresYesError(branch); } @@ -167,6 +118,113 @@ export async function maybeSetupBranchDatabase( ); } +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( + "Database setup cannot be combined with provided database env vars", + "The deploy command received --db and a DATABASE_URL or DIRECT_URL value from --env.", + "Remove the --env database value to let --db create and wire a database, or remove --db to deploy with the provided value.", + [ + "prisma-cli app deploy --db", + "prisma-cli app deploy --env DATABASE_URL=postgresql://example", + ], + "app", + ); + } + + return emptyBranchDatabaseSetupOutcome(); + } + + if (branch.kind === "production" && !options.firstProductionDeploy) { + if (options.db === true) { + throw productionDatabaseSetupAfterFirstDeployError(); + } + + return emptyBranchDatabaseSetupOutcome(); + } + + return null; +} + +function existingBranchDatabaseEnvOutcome( + context: CommandContext, + branch: BranchDatabaseDeployBranch, + targetEnvVars: string[], + envState: BranchDatabaseEnvState, + requested: boolean | undefined, +): BranchDatabaseSetupOutcome | null { + if (!hasExistingDatabaseEnvForTarget(branch, envState)) { + return null; + } + + const warning = + requested === true + ? existingDatabaseEnvWarning(branch, targetEnvVars) + : null; + if (warning) { + emitBranchDatabaseWarning(context, warning); + } + + return { + result: + requested === true + ? { + status: "skipped", + reason: existingDatabaseEnvReason(branch), + envVars: targetEnvVars, + schema: null, + } + : undefined, + warnings: warning ? [warning] : [], + }; +} + +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(); + } + + 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, + }); + + return shouldCreate ? null : emptyBranchDatabaseSetupOutcome(); +} + async function setupBranchDatabase( context: CommandContext, provider: PreviewAppProvider, @@ -196,21 +254,22 @@ async function setupBranchDatabase( let schemaSetup: BranchDatabaseSchemaSetupResult | null = null; const warnings: string[] = []; let skippedSchemaWarning: string | null = null; - if (signal.schema) { + const schema = signal.schema; + if (schema) { emitBranchDatabaseProgress( context, "pending", - `Applying database schema with ${formatSchemaSetupCommand(signal.schema.command)}`, + `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!, + schema, branch, context.runtime.cwd, ); diff --git a/packages/cli/src/lib/app/branch-database.ts b/packages/cli/src/lib/app/branch-database.ts index 8caf231..aaefa31 100644 --- a/packages/cli/src/lib/app/branch-database.ts +++ b/packages/cli/src/lib/app/branch-database.ts @@ -209,15 +209,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) { @@ -226,40 +220,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 { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + if (!SKIPPED_DIRECTORIES.has(entry.name)) { + await scanDirectory(cwd, entryPath, depth + 1, state, signal); } + return; + } - state.filesVisited += 1; + if (!entry.isFile()) { + return; + } - if (entry.name === "schema.prisma") { - state.schemaCandidates.push(entryPath); - } + state.filesVisited += 1; + collectBranchDatabaseCandidate(entryPath, entry.name, state); - if (isPrismaNextConfigFile(entry.name)) { - state.prismaNextConfigCandidates.push(entryPath); - } + if ( + await shouldRecordDatabaseUrlReference(entryPath, entry.name, state, signal) + ) { + state.databaseUrlReferences.push( + path.relative(cwd, entryPath) || entry.name, + ); + } +} - if ( - state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && - shouldScanForDatabaseUrl(entry.name) && - (await fileContainsDatabaseUrl(entryPath, signal)) - ) { - state.databaseUrlReferences.push( - path.relative(cwd, entryPath) || entry.name, - ); - } +function collectBranchDatabaseCandidate( + entryPath: string, + entryName: string, + state: ScanState, +): void { + if (entryName === "schema.prisma") { + state.schemaCandidates.push(entryPath); + } + + if (isPrismaNextConfigFile(entryName)) { + state.prismaNextConfigCandidates.push(entryPath); } } +async function shouldRecordDatabaseUrlReference( + entryPath: string, + entryName: string, + state: ScanState, + signal: AbortSignal, +): Promise { + return ( + state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && + shouldScanForDatabaseUrl(entryName) && + (await fileContainsDatabaseUrl(entryPath, signal)) + ); +} + async function selectPrismaOrmSchema( cwd: string, candidates: string[], diff --git a/packages/cli/src/lib/app/preview-build.ts b/packages/cli/src/lib/app/preview-build.ts index 3348e67..b2441ac 100644 --- a/packages/cli/src/lib/app/preview-build.ts +++ b/packages/cli/src/lib/app/preview-build.ts @@ -746,39 +746,54 @@ export async function normalizeArtifactSymlinks( continue; } - const target = await unsupportedFilesystemBoundary(signal, () => - readlink(fullPath), - ); - const resolvedTarget = path.resolve(path.dirname(fullPath), target); - - if (isPathWithin(normalizedArtifactDir, resolvedTarget)) { - continue; + const materialized = await materializeArtifactSymlink({ + fullPath, + normalizedArtifactDir, + normalizedAppPath, + signal, + }); + if (materialized === "directory") { + await walkDirectory(fullPath); } + } + } +} - if (!isPathWithin(normalizedAppPath, resolvedTarget)) { - throw new Error( - `Build artifact symlink escapes the app directory: ${resolvedTarget}`, - ); - } +async function materializeArtifactSymlink(options: { + fullPath: string; + normalizedArtifactDir: string; + normalizedAppPath: string; + signal?: AbortSignal; +}): Promise<"directory" | "file" | "internal"> { + const target = await unsupportedFilesystemBoundary(options.signal, () => + readlink(options.fullPath), + ); + const resolvedTarget = path.resolve(path.dirname(options.fullPath), target); - const targetStat = await unsupportedFilesystemBoundary(signal, () => - stat(resolvedTarget), - ); - await unsupportedFilesystemBoundary(signal, () => - rm(fullPath, { force: true, recursive: true }), - ); - await unsupportedFilesystemBoundary(signal, () => - cp(resolvedTarget, fullPath, { - recursive: targetStat.isDirectory(), - dereference: true, - }), - ); + if (isPathWithin(options.normalizedArtifactDir, resolvedTarget)) { + return "internal"; + } - if (targetStat.isDirectory()) { - await walkDirectory(fullPath); - } - } + if (!isPathWithin(options.normalizedAppPath, resolvedTarget)) { + throw new Error( + `Build artifact symlink escapes the app directory: ${resolvedTarget}`, + ); } + + const targetStat = await unsupportedFilesystemBoundary(options.signal, () => + stat(resolvedTarget), + ); + await unsupportedFilesystemBoundary(options.signal, () => + rm(options.fullPath, { force: true, recursive: true }), + ); + await unsupportedFilesystemBoundary(options.signal, () => + cp(resolvedTarget, options.fullPath, { + recursive: targetStat.isDirectory(), + dereference: true, + }), + ); + + return targetStat.isDirectory() ? "directory" : "file"; } function isPathWithin(rootPath: string, candidatePath: string): boolean { diff --git a/packages/cli/src/lib/app/preview-provider.ts b/packages/cli/src/lib/app/preview-provider.ts index d6cf95d..c6ad93a 100644 --- a/packages/cli/src/lib/app/preview-provider.ts +++ b/packages/cli/src/lib/app/preview-provider.ts @@ -1116,43 +1116,58 @@ async function findAppForDeployment( } for (const service of servicesResult.value) { - const detailResult = await sdk.showService({ - serviceId: service.id, + const app = await findServiceAppForDeployment( + sdk, + service.id, + deploymentId, signal, - }); - if (detailResult.isErr()) { - throw new Error(detailResult.error.message); + ); + if (app) { + return app; } + } + } - const app: PreviewAppRecord = { - id: detailResult.value.id, - name: detailResult.value.name, - region: detailResult.value.region ?? null, - liveDeploymentId: detailResult.value.latestVersionId ?? null, - liveUrl: toAbsoluteUrl( - detailResult.value.serviceEndpointDomain ?? null, - ), - }; + return null; +} - if (app.liveDeploymentId === deploymentId) { - return app; - } +async function findServiceAppForDeployment( + sdk: ComputeClient, + serviceId: string, + deploymentId: string, + signal?: AbortSignal, +): Promise { + const detailResult = await sdk.showService({ + serviceId, + signal, + }); + if (detailResult.isErr()) { + throw new Error(detailResult.error.message); + } - const versionsResult = await sdk.listVersions({ - serviceId: service.id, - signal, - }); - if (versionsResult.isErr()) { - throw new Error(versionsResult.error.message); - } + const app: PreviewAppRecord = { + id: detailResult.value.id, + name: detailResult.value.name, + region: detailResult.value.region ?? null, + liveDeploymentId: detailResult.value.latestVersionId ?? null, + liveUrl: toAbsoluteUrl(detailResult.value.serviceEndpointDomain ?? null), + }; - if (versionsResult.value.some((version) => version.id === deploymentId)) { - return app; - } - } + if (app.liveDeploymentId === deploymentId) { + return app; } - return null; + const versionsResult = await sdk.listVersions({ + serviceId, + signal, + }); + if (versionsResult.isErr()) { + throw new Error(versionsResult.error.message); + } + + return versionsResult.value.some((version) => version.id === deploymentId) + ? app + : null; } function toAbsoluteUrl(url: string | null): string | null { diff --git a/packages/cli/src/lib/auth/login.ts b/packages/cli/src/lib/auth/login.ts index 8cb0ba1..0221d38 100644 --- a/packages/cli/src/lib/auth/login.ts +++ b/packages/cli/src/lib/auth/login.ts @@ -173,37 +173,12 @@ async function consumePastedCallback(options: { // wrong paste shows a hint and re-asks instead of ending the whole login; // the browser callback stays open the whole time and can still win. for (;;) { - let answer: string; - try { - answer = await rl.question("Paste the callback URL here: ", { - signal: options.signal, - }); - } catch (error) { - // The browser callback won the race and aborted us. Stop prompting. - if ((error as { name?: string } | null)?.name === "AbortError") return; - throw error; - } + const url = await readPastedCallbackUrl(rl, options); + if (url === null) return; + if (url === undefined) continue; - const trimmed = answer.trim().replace(/^["']|["']$/g, ""); - let url: URL; - try { - if (!trimmed) throw new Error("empty input"); - url = new URL(trimmed); - } catch { - options.output.write( - "That didn't look like a URL. Paste the full localhost callback URL and try again.\n", - ); - continue; - } - - try { - await options.complete(url); + if (await tryCompletePastedCallback(url, options)) { return; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - options.output.write( - `Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`, - ); } } } finally { @@ -211,6 +186,49 @@ async function consumePastedCallback(options: { } } +async function readPastedCallbackUrl( + rl: readline.Interface, + options: { signal: AbortSignal; output: Writable }, +): Promise { + let answer: string; + try { + answer = await rl.question("Paste the callback URL here: ", { + signal: options.signal, + }); + } catch (error) { + // The browser callback won the race and aborted us. Stop prompting. + if ((error as { name?: string } | null)?.name === "AbortError") return null; + throw error; + } + + const trimmed = answer.trim().replace(/^["']|["']$/g, ""); + try { + if (!trimmed) throw new Error("empty input"); + return new URL(trimmed); + } catch { + options.output.write( + "That didn't look like a URL. Paste the full localhost callback URL and try again.\n", + ); + return undefined; + } +} + +async function tryCompletePastedCallback( + url: URL, + options: { complete: (url: URL) => Promise; output: Writable }, +): Promise { + try { + await options.complete(url); + return true; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + options.output.write( + `Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`, + ); + return false; + } +} + class LoginState { private latestVerifier?: string; private latestState?: string; diff --git a/packages/cli/src/lib/project/setup.ts b/packages/cli/src/lib/project/setup.ts index 2ab4ed4..386ee81 100644 --- a/packages/cli/src/lib/project/setup.ts +++ b/packages/cli/src/lib/project/setup.ts @@ -45,8 +45,9 @@ export function resolveProjectForSetup( const matches = projects.filter( (project) => project.id === projectRef || project.name === projectRef, ); - if (matches.length === 1) { - return matches[0]!; + const match = matches[0]; + if (matches.length === 1 && match) { + return match; } if (matches.length > 1) { throw projectAmbiguousError(projectRef, matches); diff --git a/packages/cli/src/presenters/app.ts b/packages/cli/src/presenters/app.ts index f71e0b0..da59ef5 100644 --- a/packages/cli/src/presenters/app.ts +++ b/packages/cli/src/presenters/app.ts @@ -94,7 +94,7 @@ function renderBranchDatabaseDeploySummary( context: CommandContext, result: AppDeployResult, ): string[] { - if (!result.branchDatabase || result.branchDatabase.status !== "created") { + if (result.branchDatabase?.status !== "created") { return []; } diff --git a/packages/cli/src/shell/command-runner.ts b/packages/cli/src/shell/command-runner.ts index 36317fb..239788a 100644 --- a/packages/cli/src/shell/command-runner.ts +++ b/packages/cli/src/shell/command-runner.ts @@ -70,57 +70,84 @@ export async function runCommand( try { const success = await handler(context); - - if (flags.json) { - writeJsonSuccess(context.output, { - ...success, - result: presenter.renderJson - ? presenter.renderJson(success.result) - : success.result, - }); + await writeCommandSuccess( + context, + descriptor, + success, + presenter, + Date.now() - startedAt, + ); + } catch (error) { + const cliError = toCliError(error, runtime); + if (cliError) { + writeCommandError(context, commandName, cliError); + process.exitCode = cliError.exitCode; return; } - const stdout = - presenter.renderStdout?.(context, descriptor, success.result) ?? []; - if (flags.quiet) { - if (stdout.length > 0) { - context.output.stdout.write(`${stdout.join("\n")}\n`); - } - return; - } + throw error; + } +} - const rendered = presenter.renderHuman(context, descriptor, success.result); - const diagnostics = await renderBestEffortCommandDiagnostics(context, { - enabled: flags.verbose && rendered.length > 0, - durationMs: Date.now() - startedAt, +async function writeCommandSuccess( + context: Awaited>, + descriptor: CommandDescriptor, + success: CommandSuccess, + presenter: CommandPresenter, + durationMs: number, +): Promise { + if (context.flags.json) { + writeJsonSuccess(context.output, { + ...success, + result: presenter.renderJson + ? presenter.renderJson(success.result) + : success.result, }); - const humanLines = [...rendered, ...diagnostics]; - if (stdout.length > 0 && humanLines.length > 0) { - humanLines.push(""); - } + return; + } - writeHumanLines(context.output, humanLines); + const stdout = + presenter.renderStdout?.(context, descriptor, success.result) ?? []; + if (context.flags.quiet) { + writeStdoutLines(context, stdout); + return; + } - if (stdout.length > 0) { - context.output.stdout.write(`${stdout.join("\n")}\n`); - } - } catch (error) { - const cliError = toCliError(error, runtime); - if (cliError) { - if (flags.json) { - writeJsonError(context.output, commandName, cliError); - } else { - writeHumanError(context.output, context.ui, cliError, { - trace: flags.trace, - }); - } + const rendered = presenter.renderHuman(context, descriptor, success.result); + const diagnostics = await renderBestEffortCommandDiagnostics(context, { + enabled: context.flags.verbose && rendered.length > 0, + durationMs, + }); + const humanLines = [...rendered, ...diagnostics]; + if (stdout.length > 0 && humanLines.length > 0) { + humanLines.push(""); + } - process.exitCode = cliError.exitCode; - return; - } + writeHumanLines(context.output, humanLines); + writeStdoutLines(context, stdout); +} - throw error; +function writeCommandError( + context: Awaited>, + commandName: string, + cliError: CliError, +): void { + if (context.flags.json) { + writeJsonError(context.output, commandName, cliError); + return; + } + + writeHumanError(context.output, context.ui, cliError, { + trace: context.flags.trace, + }); +} + +function writeStdoutLines( + context: Awaited>, + lines: string[], +): void { + if (lines.length > 0) { + context.output.stdout.write(`${lines.join("\n")}\n`); } } diff --git a/packages/cli/src/shell/help.ts b/packages/cli/src/shell/help.ts index 7f69aec..62249eb 100644 --- a/packages/cli/src/shell/help.ts +++ b/packages/cli/src/shell/help.ts @@ -31,52 +31,94 @@ export function renderHelp(command: Command, runtime: CliRuntime): string { lines.push(...renderCommandRows(rail, ui, visibleCommands)); } - if (descriptor.longDescription) { - lines.push(`${rail}`); - const wrapped = wrapText( - descriptor.longDescription, - Math.max(ui.width - CARD_PREFIX.length, 40), - ); - for (const line of wrapped) { - lines.push(`${rail} ${line}`); - } - } + lines.push(...renderLongDescription(rail, ui, descriptor.longDescription)); + lines.push( + ...renderVisibleOptions(rail, ui, visibleCommands, visibleOptions), + ); + lines.push(...renderExamples(rail, descriptor.examples)); + lines.push(...renderDocsPath(rail, ui, descriptor.docsPath)); - if (visibleOptions.length > 0) { - if (visibleCommands.length > 0) { - lines.push(`${rail}`); - } + lines.push(""); - if ( - visibleCommands.length > 0 && - visibleOptions.every((option) => - COMPACT_GLOBAL_OPTION_FLAGS.includes(option.flags), - ) - ) { - lines.push(`${rail} Global options:`); - } + return `${lines.join("\n")}`; +} - lines.push(...renderOptionRows(rail, ui, visibleOptions)); +function renderLongDescription( + rail: string, + ui: ReturnType, + longDescription: string | undefined, +): string[] { + if (!longDescription) { + return []; } - if (descriptor.examples && descriptor.examples.length > 0) { - lines.push(`${rail}`); - lines.push(`${rail} Examples:`); - for (const example of descriptor.examples) { - lines.push(`${rail} $ ${example}`); - } + return [ + `${rail}`, + ...wrapText( + longDescription, + Math.max(ui.width - CARD_PREFIX.length, 40), + ).map((line) => `${rail} ${line}`), + ]; +} + +function renderVisibleOptions( + rail: string, + ui: ReturnType, + visibleCommands: Command[], + visibleOptions: Option[], +): string[] { + if (visibleOptions.length === 0) { + return []; } - if (descriptor.docsPath) { - lines.push(`${rail}`); - lines.push( - `${rail} ${ui.accent(padDisplay("Read more", 16))} ${ui.link(descriptor.docsPath)}`, - ); + const lines = visibleCommands.length > 0 ? [`${rail}`] : []; + if (shouldLabelGlobalOptions(visibleCommands, visibleOptions)) { + lines.push(`${rail} Global options:`); } - lines.push(""); + return [...lines, ...renderOptionRows(rail, ui, visibleOptions)]; +} - return `${lines.join("\n")}`; +function shouldLabelGlobalOptions( + visibleCommands: Command[], + visibleOptions: Option[], +): boolean { + return ( + visibleCommands.length > 0 && + visibleOptions.every((option) => + COMPACT_GLOBAL_OPTION_FLAGS.includes(option.flags), + ) + ); +} + +function renderExamples( + rail: string, + examples: string[] | undefined, +): string[] { + if (!examples || examples.length === 0) { + return []; + } + + return [ + `${rail}`, + `${rail} Examples:`, + ...examples.map((example) => `${rail} $ ${example}`), + ]; +} + +function renderDocsPath( + rail: string, + ui: ReturnType, + docsPath: string | undefined, +): string[] { + if (!docsPath) { + return []; + } + + return [ + `${rail}`, + `${rail} ${ui.accent(padDisplay("Read more", 16))} ${ui.link(docsPath)}`, + ]; } function renderCommandRows( diff --git a/packages/cli/tests/app-env-vars.test.ts b/packages/cli/tests/app-env-vars.test.ts index 90c43c8..8b98a0f 100644 --- a/packages/cli/tests/app-env-vars.test.ts +++ b/packages/cli/tests/app-env-vars.test.ts @@ -87,7 +87,7 @@ describe("app env vars", () => { "API_URL=https://api.example", 'QUOTED="hello world"', "export FEATURE_FLAG=enabled", - "LITERAL=${API_URL}/v1", + "LITERAL=$" + "{API_URL}/v1", ].join("\n"), ".env", "add", @@ -96,7 +96,7 @@ describe("app env vars", () => { { key: "API_URL", value: "https://api.example" }, { key: "QUOTED", value: "hello world" }, { key: "FEATURE_FLAG", value: "enabled" }, - { key: "LITERAL", value: "${API_URL}/v1" }, + { key: "LITERAL", value: "$" + "{API_URL}/v1" }, ]); }); diff --git a/packages/cli/tests/project-real-mode.test.ts b/packages/cli/tests/project-real-mode.test.ts index 1585394..6a484a9 100644 --- a/packages/cli/tests/project-real-mode.test.ts +++ b/packages/cli/tests/project-real-mode.test.ts @@ -128,6 +128,84 @@ function scmRepositoryRecord(overrides: Record = {}) { }; } +function paginatedData(data: unknown[], nextCursor: string | null = null) { + return { + data: { + data, + pagination: { + nextCursor, + hasMore: nextCursor !== null, + }, + }, + }; +} + +function installedGithubAppGet( + pathName: string, + request?: { params?: { query?: Record } }, +) { + if (pathName === "/v1/projects") { + return mockClient().GET(pathName); + } + + if (pathName === "/v1/source-repositories") { + return sourceRepositoryList(); + } + + if (pathName === "/v1/scm-installations") { + expect(request?.params?.query).toEqual({ + workspaceId: "ws_123", + limit: 100, + }); + return paginatedData([scmInstallationRecord()]); + } + + if (pathName === "/v1/scm-installations/{installationId}/repositories") { + return request?.params?.query?.cursor === "2" + ? paginatedData([ + scmRepositoryRecord({ + id: 123456, + fullName: "prisma/prisma-cli", + isPrivate: true, + }), + ]) + : paginatedData([scmRepositoryRecord()], "2"); + } + + throw new Error(`Unexpected path ${pathName}`); +} + +function interactiveGithubAppGet( + pathName: string, + installationListCalls: number, +) { + if (pathName === "/v1/projects") { + return mockClient().GET(pathName); + } + + if (pathName === "/v1/source-repositories") { + return sourceRepositoryList(); + } + + if (pathName === "/v1/scm-installations") { + return paginatedData( + installationListCalls === 1 ? [] : [scmInstallationRecord()], + ); + } + + if (pathName === "/v1/scm-installations/{installationId}/repositories") { + return paginatedData([ + scmRepositoryRecord({ + id: 123456, + fullName: "prisma/prisma-cli", + isPrivate: true, + }), + ]); + } + + throw new Error(`Unexpected path ${pathName}`); +} + describe("real project mode", () => { it("uses the real API path for project list and sorts by name then id", async () => { const readAuthState = mockAuthState(); @@ -248,89 +326,7 @@ describe("real project mode", () => { ( pathName: string, request?: { params?: { query?: Record } }, - ) => { - if (pathName === "/v1/projects") { - return mockClient().GET(pathName); - } - - if (pathName === "/v1/source-repositories") { - return sourceRepositoryList(); - } - - if (pathName === "/v1/scm-installations") { - expect(request?.params?.query).toEqual({ - workspaceId: "ws_123", - limit: 100, - }); - return { - data: { - data: [ - { - id: "scminstall_123", - type: "scm-installation", - url: "https://api.prisma.test/v1/scm-installations/scminstall_123", - provider: "github", - installationId: 98765, - accountId: 111, - accountLogin: "prisma", - accountType: "organization", - suspended: false, - createdAt: "2026-05-18T00:00:00.000Z", - updatedAt: "2026-05-18T00:00:00.000Z", - }, - ], - pagination: { - nextCursor: null, - hasMore: false, - }, - }, - }; - } - - if ( - pathName === "/v1/scm-installations/{installationId}/repositories" - ) { - if (request?.params?.query?.cursor === "2") { - return { - data: { - data: [ - { - id: 123456, - type: "scm-repository", - fullName: "prisma/prisma-cli", - defaultBranch: "main", - isPrivate: true, - }, - ], - pagination: { - nextCursor: null, - hasMore: false, - }, - }, - }; - } - - return { - data: { - data: [ - { - id: 999, - type: "scm-repository", - fullName: "prisma/other", - defaultBranch: "main", - isPrivate: false, - }, - ], - pagination: { - nextCursor: "2", - hasMore: true, - }, - }, - }; - } - - throw new Error(`Unexpected path ${pathName}`); - }, + ) => installedGithubAppGet(pathName, request), ); const post = vi .fn() @@ -700,65 +696,10 @@ describe("real project mode", () => { let installationListCalls = 0; const get = vi.fn().mockImplementation((pathName: string) => { - if (pathName === "/v1/projects") { - return mockClient().GET(pathName); - } - - if (pathName === "/v1/source-repositories") { - return sourceRepositoryList(); - } - if (pathName === "/v1/scm-installations") { installationListCalls += 1; - return { - data: { - data: - installationListCalls === 1 - ? [] - : [ - { - id: "scminstall_123", - type: "scm-installation", - url: "https://api.prisma.test/v1/scm-installations/scminstall_123", - provider: "github", - installationId: 98765, - accountId: 111, - accountLogin: "prisma", - accountType: "organization", - suspended: false, - createdAt: "2026-05-18T00:00:00.000Z", - updatedAt: "2026-05-18T00:00:00.000Z", - }, - ], - pagination: { - nextCursor: null, - hasMore: false, - }, - }, - }; } - - if (pathName === "/v1/scm-installations/{installationId}/repositories") { - return { - data: { - data: [ - { - id: 123456, - type: "scm-repository", - fullName: "prisma/prisma-cli", - defaultBranch: "main", - isPrivate: true, - }, - ], - pagination: { - nextCursor: null, - hasMore: false, - }, - }, - }; - } - - throw new Error(`Unexpected path ${pathName}`); + return interactiveGithubAppGet(pathName, installationListCalls); }); const post = vi .fn() diff --git a/packages/compute/tests/scale-to-zero.test.ts b/packages/compute/tests/scale-to-zero.test.ts index 8bead5d..8c128d2 100644 --- a/packages/compute/tests/scale-to-zero.test.ts +++ b/packages/compute/tests/scale-to-zero.test.ts @@ -86,7 +86,7 @@ describe("scale-to-zero guard", () => { it("waitUntil signal abort releases before a still-pending promise settles", async () => { const { file } = await createControlFile(); const controller = new AbortController(); - let resolvePromise: (value: string) => void; + let resolvePromise: ((value: string) => void) | undefined; const promise = new Promise((resolve) => { resolvePromise = resolve; }); @@ -97,7 +97,8 @@ describe("scale-to-zero guard", () => { controller.abort(); expect(await readSignals(file)).toBe("+-"); - resolvePromise!("done"); + expect(resolvePromise).toBeDefined(); + resolvePromise("done"); await expect(promise).resolves.toBe("done"); await Promise.resolve(); expect(await readSignals(file)).toBe("+-"); From 94b1548c0618eb83466d7162d83b3c1b0de221ef Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:38:38 -0400 Subject: [PATCH 04/28] enable no foreach lint rule --- biome.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 91b68c4..ad32400 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -18,10 +18,10 @@ "rules": { "recommended": true, "complexity": { - "noExcessiveCognitiveComplexity": "on" + "noExcessiveCognitiveComplexity": "on", + "noForEach": "on" } // "complexity": { - // "noForEach": "on", // "noImplicitCoercions": "on" // }, // "nursery": { From d814e485d3c92685bb26f3023e472b01c3c28883 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:39:13 -0400 Subject: [PATCH 05/28] enable no implicit coercions lint rule --- biome.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index ad32400..579a8dd 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -19,10 +19,10 @@ "recommended": true, "complexity": { "noExcessiveCognitiveComplexity": "on", - "noForEach": "on" + "noForEach": "on", + "noImplicitCoercions": "on" } // "complexity": { - // "noImplicitCoercions": "on" // }, // "nursery": { // "noConditionalExpect": "on", From 36d2ff5bfbf3d54041852bdada1d058864529689 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:43:30 -0400 Subject: [PATCH 06/28] enable no conditional expect lint rule --- biome.jsonc | 4 +- packages/cli/tests/app-env-vars.test.ts | 26 +- packages/cli/tests/project-real-mode.test.ts | 278 ++++++++++--------- 3 files changed, 164 insertions(+), 144 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 579a8dd..0f297d3 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -21,11 +21,13 @@ "noExcessiveCognitiveComplexity": "on", "noForEach": "on", "noImplicitCoercions": "on" + }, + "nursery": { + "noConditionalExpect": "on" } // "complexity": { // }, // "nursery": { - // "noConditionalExpect": "on", // "noFloatingPromises": "on", // "noForIn": "on", // "noLoopFunc": "on", diff --git a/packages/cli/tests/app-env-vars.test.ts b/packages/cli/tests/app-env-vars.test.ts index 8b98a0f..6b031a8 100644 --- a/packages/cli/tests/app-env-vars.test.ts +++ b/packages/cli/tests/app-env-vars.test.ts @@ -244,20 +244,30 @@ describe("app env vars", () => { }), ); - try { + expect(() => parseEnvAssignments( ["DATABASE_URL=postgresql://first", "DATABASE_URL=postgresql://second"], { commandName: "deploy" }, - ); - } catch (error) { - expect(error).toMatchObject({ + ), + ).toThrowError( + expect.objectContaining({ code: "USAGE_ERROR", summary: 'Environment variable "DATABASE_URL" was provided more than once', - }); - expect(JSON.stringify(error)).not.toContain("postgresql://first"); - expect(JSON.stringify(error)).not.toContain("postgresql://second"); - } + }), + ); + expect(() => + parseEnvAssignments( + ["DATABASE_URL=postgresql://first", "DATABASE_URL=postgresql://second"], + { commandName: "deploy" }, + ), + ).toThrowError(expect.not.stringContaining("postgresql://first")); + expect(() => + parseEnvAssignments( + ["DATABASE_URL=postgresql://first", "DATABASE_URL=postgresql://second"], + { commandName: "deploy" }, + ), + ).toThrowError(expect.not.stringContaining("postgresql://second")); }); it("returns sorted environment variable names only", async () => { diff --git a/packages/cli/tests/project-real-mode.test.ts b/packages/cli/tests/project-real-mode.test.ts index 6a484a9..5d01ad1 100644 --- a/packages/cli/tests/project-real-mode.test.ts +++ b/packages/cli/tests/project-real-mode.test.ts @@ -153,10 +153,6 @@ function installedGithubAppGet( } if (pathName === "/v1/scm-installations") { - expect(request?.params?.query).toEqual({ - workspaceId: "ws_123", - limit: 100, - }); return paginatedData([scmInstallationRecord()]); } @@ -206,6 +202,50 @@ function interactiveGithubAppGet( throw new Error(`Unexpected path ${pathName}`); } +function expectScmInstallationsListed( + get: ReturnType, + signal: AbortSignal, +): void { + expect(get).toHaveBeenCalledWith( + "/v1/scm-installations", + expect.objectContaining({ + params: { + query: { + workspaceId: "ws_123", + limit: 100, + }, + }, + signal, + }), + ); +} + +function expectInstallIntentPost(post: ReturnType): void { + expect(post).toHaveBeenCalledWith( + "/v1/scm-installations/install-intents", + expect.objectContaining({ + body: { + provider: "github", + workspaceId: "ws_123", + }, + }), + ); +} + +function expectSourceRepositoryPost(post: ReturnType): void { + expect(post).toHaveBeenCalledWith( + "/v1/source-repositories", + expect.objectContaining({ + body: { + projectId: "proj_123", + provider: "github", + providerRepositoryId: 123456, + installationId: "scminstall_123", + }, + }), + ); +} + describe("real project mode", () => { it("uses the real API path for project list and sorts by name then id", async () => { const readAuthState = mockAuthState(); @@ -328,37 +368,29 @@ describe("real project mode", () => { request?: { params?: { query?: Record } }, ) => installedGithubAppGet(pathName, request), ); - const post = vi - .fn() - .mockImplementation((pathName: string, request?: { body?: unknown }) => { - if (pathName === "/v1/source-repositories") { - expect(request?.body).toEqual({ - projectId: "proj_123", - provider: "github", - providerRepositoryId: 123456, - installationId: "scminstall_123", - }); - return { + const post = vi.fn().mockImplementation((pathName: string) => { + if (pathName === "/v1/source-repositories") { + return { + data: { data: { - data: { - id: "srcrepo_123", - repoId: 123456, - provider: "github", - repoFullName: "prisma/prisma-cli", - defaultBranch: "main", - isPrivate: true, - status: "active", - projectId: "proj_123", - installationId: "scminstall_123", - createdAt: "2026-05-18T00:00:00.000Z", - updatedAt: "2026-05-18T00:00:00.000Z", - }, + id: "srcrepo_123", + repoId: 123456, + provider: "github", + repoFullName: "prisma/prisma-cli", + defaultBranch: "main", + isPrivate: true, + status: "active", + projectId: "proj_123", + installationId: "scminstall_123", + createdAt: "2026-05-18T00:00:00.000Z", + updatedAt: "2026-05-18T00:00:00.000Z", }, - }; - } + }, + }; + } - throw new Error(`Unexpected path ${pathName}`); - }); + throw new Error(`Unexpected path ${pathName}`); + }); vi.doMock("../src/lib/auth/auth-ops", () => ({ readAuthState: mockAuthState(), @@ -393,6 +425,8 @@ describe("real project mode", () => { ); expect(post).toHaveBeenCalledOnce(); + expectSourceRepositoryPost(post); + expectScmInstallationsListed(get, context.runtime.signal); expect(get).toHaveBeenCalledWith( "/v1/scm-installations/{installationId}/repositories", { @@ -508,29 +542,23 @@ describe("real project mode", () => { throw new Error(`Unexpected path ${pathName}`); }); - const post = vi - .fn() - .mockImplementation((pathName: string, request?: { body?: unknown }) => { - if (pathName === "/v1/scm-installations/install-intents") { - expect(request?.body).toEqual({ - provider: "github", - workspaceId: "ws_123", - }); - return { + const post = vi.fn().mockImplementation((pathName: string) => { + if (pathName === "/v1/scm-installations/install-intents") { + return { + data: { data: { - data: { - type: "install-intent", - provider: "github", - workspaceId: "wksp_123", - installUrl: - "https://github.com/apps/prisma/installations/new?state=abc", - }, + type: "install-intent", + provider: "github", + workspaceId: "wksp_123", + installUrl: + "https://github.com/apps/prisma/installations/new?state=abc", }, - }; - } + }, + }; + } - throw new Error(`Unexpected path ${pathName}`); - }); + throw new Error(`Unexpected path ${pathName}`); + }); vi.doMock("../src/lib/auth/auth-ops", () => ({ readAuthState: mockAuthState(), @@ -571,6 +599,7 @@ describe("real project mode", () => { repository: "prisma/prisma-cli", }, }); + expectInstallIntentPost(post); }); it("creates an install intent when the stored GitHub App installation is unavailable", async () => { @@ -624,29 +653,23 @@ describe("real project mode", () => { throw new Error(`Unexpected path ${pathName}`); }); - const post = vi - .fn() - .mockImplementation((pathName: string, request?: { body?: unknown }) => { - if (pathName === "/v1/scm-installations/install-intents") { - expect(request?.body).toEqual({ - provider: "github", - workspaceId: "ws_123", - }); - return { + const post = vi.fn().mockImplementation((pathName: string) => { + if (pathName === "/v1/scm-installations/install-intents") { + return { + data: { data: { - data: { - type: "install-intent", - provider: "github", - workspaceId: "wksp_123", - installUrl: - "https://github.com/apps/prisma/installations/new?state=abc", - }, + type: "install-intent", + provider: "github", + workspaceId: "wksp_123", + installUrl: + "https://github.com/apps/prisma/installations/new?state=abc", }, - }; - } + }, + }; + } - throw new Error(`Unexpected path ${pathName}`); - }); + throw new Error(`Unexpected path ${pathName}`); + }); vi.doMock("../src/lib/auth/auth-ops", () => ({ readAuthState: mockAuthState(), @@ -688,6 +711,7 @@ describe("real project mode", () => { }, }); expect(post).toHaveBeenCalledOnce(); + expectInstallIntentPost(post); }); it("waits for GitHub App installation in interactive mode and connects after approval", async () => { @@ -701,55 +725,43 @@ describe("real project mode", () => { } return interactiveGithubAppGet(pathName, installationListCalls); }); - const post = vi - .fn() - .mockImplementation((pathName: string, request?: { body?: unknown }) => { - if (pathName === "/v1/scm-installations/install-intents") { - expect(request?.body).toEqual({ - provider: "github", - workspaceId: "ws_123", - }); - return { + const post = vi.fn().mockImplementation((pathName: string) => { + if (pathName === "/v1/scm-installations/install-intents") { + return { + data: { data: { - data: { - type: "install-intent", - provider: "github", - workspaceId: "wksp_123", - installUrl: - "https://github.com/apps/prisma/installations/new?state=abc", - }, + type: "install-intent", + provider: "github", + workspaceId: "wksp_123", + installUrl: + "https://github.com/apps/prisma/installations/new?state=abc", }, - }; - } + }, + }; + } - if (pathName === "/v1/source-repositories") { - expect(request?.body).toEqual({ - projectId: "proj_123", - provider: "github", - providerRepositoryId: 123456, - installationId: "scminstall_123", - }); - return { + if (pathName === "/v1/source-repositories") { + return { + data: { data: { - data: { - id: "srcrepo_123", - repoId: 123456, - provider: "github", - repoFullName: "prisma/prisma-cli", - defaultBranch: "main", - isPrivate: true, - status: "active", - projectId: "proj_123", - installationId: "scminstall_123", - createdAt: "2026-05-18T00:00:00.000Z", - updatedAt: "2026-05-18T00:00:00.000Z", - }, + id: "srcrepo_123", + repoId: 123456, + provider: "github", + repoFullName: "prisma/prisma-cli", + defaultBranch: "main", + isPrivate: true, + status: "active", + projectId: "proj_123", + installationId: "scminstall_123", + createdAt: "2026-05-18T00:00:00.000Z", + updatedAt: "2026-05-18T00:00:00.000Z", }, - }; - } + }, + }; + } - throw new Error(`Unexpected path ${pathName}`); - }); + throw new Error(`Unexpected path ${pathName}`); + }); vi.doMock("../src/lib/auth/auth-ops", () => ({ readAuthState: mockAuthState(), @@ -790,6 +802,7 @@ describe("real project mode", () => { expect(openBrowser).toHaveBeenCalledWith( "https://github.com/apps/prisma/installations/new?state=abc", ); + expectInstallIntentPost(post); expect(installationListCalls).toBe(2); expect(post).toHaveBeenCalledWith("/v1/source-repositories", { body: { @@ -866,29 +879,23 @@ describe("real project mode", () => { throw new Error(`Unexpected path ${pathName}`); }); - const post = vi - .fn() - .mockImplementation((pathName: string, request?: { body?: unknown }) => { - if (pathName === "/v1/scm-installations/install-intents") { - expect(request?.body).toEqual({ - provider: "github", - workspaceId: "ws_123", - }); - return { + const post = vi.fn().mockImplementation((pathName: string) => { + if (pathName === "/v1/scm-installations/install-intents") { + return { + data: { data: { - data: { - type: "install-intent", - provider: "github", - workspaceId: "wksp_123", - installUrl: - "https://github.com/apps/prisma/installations/new?state=abc", - }, + type: "install-intent", + provider: "github", + workspaceId: "wksp_123", + installUrl: + "https://github.com/apps/prisma/installations/new?state=abc", }, - }; - } + }, + }; + } - throw new Error(`Unexpected path ${pathName}`); - }); + throw new Error(`Unexpected path ${pathName}`); + }); vi.doMock("../src/lib/auth/auth-ops", () => ({ readAuthState: mockAuthState(), @@ -930,6 +937,7 @@ describe("real project mode", () => { }, }); expect(post).toHaveBeenCalledOnce(); + expectInstallIntentPost(post); }); it("guards repeated GitHub App installation pagination cursors", async () => { From b84856bce6da51fae63aa3b20aabbeabe1fa41e1 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:45:03 -0400 Subject: [PATCH 07/28] enable no floating promises lint rule --- biome.jsonc | 4 ++-- packages/cli/src/bin.ts | 19 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 0f297d3..76b100d 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -23,12 +23,12 @@ "noImplicitCoercions": "on" }, "nursery": { - "noConditionalExpect": "on" + "noConditionalExpect": "on", + "noFloatingPromises": "on" } // "complexity": { // }, // "nursery": { - // "noFloatingPromises": "on", // "noForIn": "on", // "noLoopFunc": "on", // "useDisposables": "on" diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index ab758ce..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,12 +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(() => { - process.off("SIGINT", abortCli); - process.off("SIGTERM", abortCli); - }); + try { + process.exitCode = await runCli({ signal: controller.signal }); + } finally { + process.off("SIGINT", abortCli); + process.off("SIGTERM", abortCli); + } } From 7e202bf0efa232d6aeec91f08ef2a2667487b090 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:45:35 -0400 Subject: [PATCH 08/28] enable no for in lint rule --- biome.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 76b100d..b415c72 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -24,12 +24,12 @@ }, "nursery": { "noConditionalExpect": "on", - "noFloatingPromises": "on" + "noFloatingPromises": "on", + "noForIn": "on" } // "complexity": { // }, // "nursery": { - // "noForIn": "on", // "noLoopFunc": "on", // "useDisposables": "on" // }, From 2100fd559c7593a067e110287b265531915d4f99 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:46:09 -0400 Subject: [PATCH 09/28] enable no loop func lint rule --- biome.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index b415c72..9a04b12 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -25,12 +25,12 @@ "nursery": { "noConditionalExpect": "on", "noFloatingPromises": "on", - "noForIn": "on" + "noForIn": "on", + "noLoopFunc": "on" } // "complexity": { // }, // "nursery": { - // "noLoopFunc": "on", // "useDisposables": "on" // }, // "performance": { From 48f0d4c10d3b1e2dc6c6ad9875e8dd8b34fd858d Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:47:32 -0400 Subject: [PATCH 10/28] enable disposable lint rule --- biome.jsonc | 4 ++-- packages/compute/src/scale-to-zero.ts | 1 + packages/compute/tests/scale-to-zero.test.ts | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 9a04b12..3663944 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -26,12 +26,12 @@ "noConditionalExpect": "on", "noFloatingPromises": "on", "noForIn": "on", - "noLoopFunc": "on" + "noLoopFunc": "on", + "useDisposables": "on" } // "complexity": { // }, // "nursery": { - // "useDisposables": "on" // }, // "performance": { // "noAwaitInLoops": "on", diff --git a/packages/compute/src/scale-to-zero.ts b/packages/compute/src/scale-to-zero.ts index 1be3513..f3c0755 100644 --- a/packages/compute/src/scale-to-zero.ts +++ b/packages/compute/src/scale-to-zero.ts @@ -137,6 +137,7 @@ export function waitUntil( promise: PromiseLike, options?: ScaleToZeroGuardOptions, ): void { + // biome-ignore lint/nursery/useDisposables: waitUntil transfers guard cleanup to the promise finally handler. const guard = new ScaleToZeroGuard(options); // Do not attach a catch here; callers rely on the underlying promise keeping diff --git a/packages/compute/tests/scale-to-zero.test.ts b/packages/compute/tests/scale-to-zero.test.ts index 8c128d2..ade4039 100644 --- a/packages/compute/tests/scale-to-zero.test.ts +++ b/packages/compute/tests/scale-to-zero.test.ts @@ -38,7 +38,7 @@ describe("scale-to-zero guard", () => { it("releases only once when release is called multiple times", async () => { const { file } = await createControlFile(); - const guard = new ScaleToZeroGuard(); + using guard = new ScaleToZeroGuard(); guard.release(); guard.release(); @@ -51,7 +51,7 @@ describe("scale-to-zero guard", () => { const { file } = await createControlFile(); const controller = new AbortController(); - const guard = new ScaleToZeroGuard({ signal: controller.signal }); + using guard = new ScaleToZeroGuard({ signal: controller.signal }); expect(await readSignals(file)).toBe("+"); controller.abort(); @@ -66,7 +66,7 @@ describe("scale-to-zero guard", () => { const controller = new AbortController(); controller.abort(); - const guard = new ScaleToZeroGuard({ signal: controller.signal }); + using guard = new ScaleToZeroGuard({ signal: controller.signal }); guard.release(); expect(await readSignals(file)).toBe(""); @@ -117,7 +117,7 @@ describe("scale-to-zero guard", () => { it("removes the abort listener after manual release", async () => { const { file } = await createControlFile(); const controller = new AbortController(); - const guard = new ScaleToZeroGuard({ signal: controller.signal }); + using guard = new ScaleToZeroGuard({ signal: controller.signal }); guard.release(); controller.abort(); From ae87e2b1d60f100363de4209460a5473dcad9b2d Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:51:43 -0400 Subject: [PATCH 11/28] enable no await in loops lint rule --- biome.jsonc | 4 +++- packages/cli/src/adapters/token-storage.ts | 1 + packages/cli/src/controllers/app-env-file.ts | 1 + packages/cli/src/controllers/app-env.ts | 1 + packages/cli/src/controllers/app.ts | 1 + packages/cli/src/controllers/branch.ts | 1 + packages/cli/src/controllers/project.ts | 1 + packages/cli/src/lib/app/branch-database.ts | 1 + packages/cli/src/lib/app/env-vars.ts | 1 + packages/cli/src/lib/app/local-dev.ts | 1 + packages/cli/src/lib/app/preview-branch-database.ts | 1 + packages/cli/src/lib/app/preview-build-settings.ts | 1 + packages/cli/src/lib/app/preview-build.ts | 1 + packages/cli/src/lib/app/preview-provider.ts | 1 + packages/cli/src/lib/auth/login.ts | 1 + packages/cli/src/lib/database/provider.ts | 1 + packages/cli/tests/app-build.test.ts | 1 + packages/cli/tests/app-controller.test.ts | 1 + packages/cli/tests/helpers.ts | 1 + 19 files changed, 21 insertions(+), 1 deletion(-) diff --git a/biome.jsonc b/biome.jsonc index 3663944..d9ce406 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -28,13 +28,15 @@ "noForIn": "on", "noLoopFunc": "on", "useDisposables": "on" + }, + "performance": { + "noAwaitInLoops": "on" } // "complexity": { // }, // "nursery": { // }, // "performance": { - // "noAwaitInLoops": "on", // "noBarrelFile": "on", // "useTopLevelRegex": "on" // }, diff --git a/packages/cli/src/adapters/token-storage.ts b/packages/cli/src/adapters/token-storage.ts index 8e0b36b..ba9bff6 100644 --- a/packages/cli/src/adapters/token-storage.ts +++ b/packages/cli/src/adapters/token-storage.ts @@ -1,3 +1,4 @@ +// 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"; diff --git a/packages/cli/src/controllers/app-env-file.ts b/packages/cli/src/controllers/app-env-file.ts index 095bdd8..14000f4 100644 --- a/packages/cli/src/controllers/app-env-file.ts +++ b/packages/cli/src/controllers/app-env-file.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Environment variable mutations and lookups are intentionally sequential. import type { ManagementApiClient } from "@prisma/management-api-sdk"; import { type EnvScope, formatScopeLabel } from "../lib/app/env-config"; diff --git a/packages/cli/src/controllers/app-env.ts b/packages/cli/src/controllers/app-env.ts index 9a79543..a101052 100644 --- a/packages/cli/src/controllers/app-env.ts +++ b/packages/cli/src/controllers/app-env.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: API pagination loops are intentionally sequential. import type { ManagementApiClient } from "@prisma/management-api-sdk"; import { diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index 5052f0c..7af38fc 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Polling and ordered filesystem probes are intentionally sequential. import { access, readFile } from "node:fs/promises"; import path from "node:path"; import type { PortMapping, StreamRecord } from "@prisma/compute-sdk"; diff --git a/packages/cli/src/controllers/branch.ts b/packages/cli/src/controllers/branch.ts index ee85d0a..0e1a7d0 100644 --- a/packages/cli/src/controllers/branch.ts +++ b/packages/cli/src/controllers/branch.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Branch pagination requests must run sequentially. import type { ManagementApiClient } from "@prisma/management-api-sdk"; import { requireComputeAuth } from "../lib/auth/guard"; import { diff --git a/packages/cli/src/controllers/project.ts b/packages/cli/src/controllers/project.ts index cc6135e..82d9803 100644 --- a/packages/cli/src/controllers/project.ts +++ b/packages/cli/src/controllers/project.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: SCM pagination, polling, and ordered fallback checks are intentionally sequential. import type { ManagementApiClient } from "@prisma/management-api-sdk"; import { matchError } from "better-result"; import open from "open"; diff --git a/packages/cli/src/lib/app/branch-database.ts b/packages/cli/src/lib/app/branch-database.ts index aaefa31..911e617 100644 --- a/packages/cli/src/lib/app/branch-database.ts +++ b/packages/cli/src/lib/app/branch-database.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Schema setup and filesystem scans are intentionally sequential. import { spawn } from "node:child_process"; import type { Dirent } from "node:fs"; import { access, readdir, readFile, stat } from "node:fs/promises"; diff --git a/packages/cli/src/lib/app/env-vars.ts b/packages/cli/src/lib/app/env-vars.ts index 41b2a23..e648445 100644 --- a/packages/cli/src/lib/app/env-vars.ts +++ b/packages/cli/src/lib/app/env-vars.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Env file expansion preserves assignment order. import { usageError } from "../../shell/errors"; import { validateKey } from "./env-config"; import { readEnvFileAssignments } from "./env-file"; diff --git a/packages/cli/src/lib/app/local-dev.ts b/packages/cli/src/lib/app/local-dev.ts index 2c0e624..08d8a5c 100644 --- a/packages/cli/src/lib/app/local-dev.ts +++ b/packages/cli/src/lib/app/local-dev.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Local app detection and command fallbacks must short-circuit sequentially. import { type SpawnOptions, spawn } from "node:child_process"; import { access } from "node:fs/promises"; import path from "node:path"; diff --git a/packages/cli/src/lib/app/preview-branch-database.ts b/packages/cli/src/lib/app/preview-branch-database.ts index a19e02d..0913004 100644 --- a/packages/cli/src/lib/app/preview-branch-database.ts +++ b/packages/cli/src/lib/app/preview-branch-database.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Environment variable pagination must run sequentially. import type { ManagementApiClient } from "@prisma/management-api-sdk"; export interface PreviewEnvironmentVariableRecord { diff --git a/packages/cli/src/lib/app/preview-build-settings.ts b/packages/cli/src/lib/app/preview-build-settings.ts index 9c687eb..60e7dc1 100644 --- a/packages/cli/src/lib/app/preview-build-settings.ts +++ b/packages/cli/src/lib/app/preview-build-settings.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Config discovery probes ordered candidates sequentially. import { exec } from "node:child_process"; import { readdir, readFile, stat, writeFile } from "node:fs/promises"; import path from "node:path"; diff --git a/packages/cli/src/lib/app/preview-build.ts b/packages/cli/src/lib/app/preview-build.ts index b2441ac..723ab31 100644 --- a/packages/cli/src/lib/app/preview-build.ts +++ b/packages/cli/src/lib/app/preview-build.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Build strategy probing and filesystem traversal are intentionally sequential. import { chmod, copyFile, diff --git a/packages/cli/src/lib/app/preview-provider.ts b/packages/cli/src/lib/app/preview-provider.ts index c6ad93a..e8a0b62 100644 --- a/packages/cli/src/lib/app/preview-provider.ts +++ b/packages/cli/src/lib/app/preview-provider.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: API pagination and deployment lookup scans are intentionally sequential. import path from "node:path"; import type { PortMapping, StreamRecord } from "@prisma/compute-sdk"; import { diff --git a/packages/cli/src/lib/auth/login.ts b/packages/cli/src/lib/auth/login.ts index 0221d38..158ecf4 100644 --- a/packages/cli/src/lib/auth/login.ts +++ b/packages/cli/src/lib/auth/login.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Interactive paste prompting must retry sequentially. import events from "node:events"; import http from "node:http"; import type { AddressInfo } from "node:net"; diff --git a/packages/cli/src/lib/database/provider.ts b/packages/cli/src/lib/database/provider.ts index 90d573e..4734f34 100644 --- a/packages/cli/src/lib/database/provider.ts +++ b/packages/cli/src/lib/database/provider.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Database pagination requests must run sequentially. import type { ManagementApiClient } from "@prisma/management-api-sdk"; import { CliError } from "../../shell/errors"; diff --git a/packages/cli/tests/app-build.test.ts b/packages/cli/tests/app-build.test.ts index f2dfc5f..6d46ff2 100644 --- a/packages/cli/tests/app-build.test.ts +++ b/packages/cli/tests/app-build.test.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Table-driven test cases create isolated temp directories sequentially. import { access, lstat, diff --git a/packages/cli/tests/app-controller.test.ts b/packages/cli/tests/app-controller.test.ts index cc1bd4c..85173db 100644 --- a/packages/cli/tests/app-controller.test.ts +++ b/packages/cli/tests/app-controller.test.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Test fixture file writes are ordered for deterministic setup. import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; diff --git a/packages/cli/tests/helpers.ts b/packages/cli/tests/helpers.ts index 0ca5322..cb8e6fa 100644 --- a/packages/cli/tests/helpers.ts +++ b/packages/cli/tests/helpers.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/noAwaitInLoops: Simulated typing intentionally delays each character sequentially. import { mkdtemp, readFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; From 8369f4ce95298c6004ecc4099619e5709b1fab11 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:52:58 -0400 Subject: [PATCH 12/28] enable no barrel file lint rule --- biome.jsonc | 4 ++-- packages/cli/src/lib/app/preview-build.ts | 1 + packages/cli/src/lib/project/setup.ts | 1 + packages/compute/src/index.ts | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index d9ce406..4201cb7 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -30,14 +30,14 @@ "useDisposables": "on" }, "performance": { - "noAwaitInLoops": "on" + "noAwaitInLoops": "on", + "noBarrelFile": "on" } // "complexity": { // }, // "nursery": { // }, // "performance": { - // "noBarrelFile": "on", // "useTopLevelRegex": "on" // }, // "style": { diff --git a/packages/cli/src/lib/app/preview-build.ts b/packages/cli/src/lib/app/preview-build.ts index 723ab31..646355a 100644 --- a/packages/cli/src/lib/app/preview-build.ts +++ b/packages/cli/src/lib/app/preview-build.ts @@ -36,6 +36,7 @@ import { runResolvedBuildCommand, } from "./preview-build-settings"; +// biome-ignore lint/performance/noBarrelFile: Preview build settings are re-exported from this public app build module. export { PRISMA_APP_CONFIG_FILENAME, PRISMA_APP_CONFIG_SCHEMA_URL, diff --git a/packages/cli/src/lib/project/setup.ts b/packages/cli/src/lib/project/setup.ts index 386ee81..a235cd2 100644 --- a/packages/cli/src/lib/project/setup.ts +++ b/packages/cli/src/lib/project/setup.ts @@ -16,6 +16,7 @@ import { projectNotFoundError, } from "./resolution"; +// biome-ignore lint/performance/noBarrelFile: Project setup exposes command formatting for related project flows. export { formatCommandArgument } from "../../shell/command-arguments"; export type ProjectDirectoryBindingError = diff --git a/packages/compute/src/index.ts b/packages/compute/src/index.ts index c4c4af0..9b2ee55 100644 --- a/packages/compute/src/index.ts +++ b/packages/compute/src/index.ts @@ -1,3 +1,4 @@ +// biome-ignore lint/performance/noBarrelFile: Package entrypoint intentionally re-exports the public API. export { ScaleToZeroGuard, type ScaleToZeroGuardOptions, From 0be7bb9b0354ea89c8052c563faac8fbe099294d Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:55:48 -0400 Subject: [PATCH 13/28] enable top level regex lint rule --- biome.jsonc | 4 ++-- packages/cli/src/adapters/git.ts | 1 + packages/cli/src/controllers/app.ts | 1 + packages/cli/src/lib/app/branch-database.ts | 1 + packages/cli/src/lib/app/env-file.ts | 1 + packages/cli/src/lib/app/local-dev.ts | 1 + packages/cli/src/lib/app/preview-provider.ts | 1 + packages/cli/src/lib/project/local-pin.ts | 1 + packages/cli/src/lib/project/resolution.ts | 1 + packages/cli/src/lib/project/setup.ts | 1 + packages/cli/src/lib/version.ts | 1 + packages/cli/src/shell/command-arguments.ts | 1 + packages/cli/src/shell/update-check.ts | 1 + packages/cli/tests/app-env.test.ts | 1 + packages/cli/tests/shell.test.ts | 1 + packages/cli/tests/version.test.ts | 1 + scripts/resolve-cli-version.mjs | 1 + scripts/validate-skills.mjs | 1 + 18 files changed, 19 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 4201cb7..07979b6 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -31,14 +31,14 @@ }, "performance": { "noAwaitInLoops": "on", - "noBarrelFile": "on" + "noBarrelFile": "on", + "useTopLevelRegex": "on" } // "complexity": { // }, // "nursery": { // }, // "performance": { - // "useTopLevelRegex": "on" // }, // "style": { // "useCollapsedElseIf": "on", diff --git a/packages/cli/src/adapters/git.ts b/packages/cli/src/adapters/git.ts index 27a9712..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"; diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index 7af38fc..699a8a2 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -1,4 +1,5 @@ // 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. import { access, readFile } from "node:fs/promises"; import path from "node:path"; import type { PortMapping, StreamRecord } from "@prisma/compute-sdk"; diff --git a/packages/cli/src/lib/app/branch-database.ts b/packages/cli/src/lib/app/branch-database.ts index 911e617..621456e 100644 --- a/packages/cli/src/lib/app/branch-database.ts +++ b/packages/cli/src/lib/app/branch-database.ts @@ -1,4 +1,5 @@ // 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. import { spawn } from "node:child_process"; import type { Dirent } from "node:fs"; import { access, readdir, readFile, stat } from "node:fs/promises"; diff --git a/packages/cli/src/lib/app/env-file.ts b/packages/cli/src/lib/app/env-file.ts index 755ddaa..d287e74 100644 --- a/packages/cli/src/lib/app/env-file.ts +++ b/packages/cli/src/lib/app/env-file.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/useTopLevelRegex: Existing dotenv parsing regexes are kept inline for readability. import { readFile } from "node:fs/promises"; import path from "node:path"; import { parse as parseDotenv } from "dotenv"; diff --git a/packages/cli/src/lib/app/local-dev.ts b/packages/cli/src/lib/app/local-dev.ts index 08d8a5c..e5c46ed 100644 --- a/packages/cli/src/lib/app/local-dev.ts +++ b/packages/cli/src/lib/app/local-dev.ts @@ -1,4 +1,5 @@ // biome-ignore-all lint/performance/noAwaitInLoops: Local app detection and command fallbacks must short-circuit sequentially. +// biome-ignore-all lint/performance/useTopLevelRegex: Existing package script regexes are kept inline for readability. import { type SpawnOptions, spawn } from "node:child_process"; import { access } from "node:fs/promises"; import path from "node:path"; diff --git a/packages/cli/src/lib/app/preview-provider.ts b/packages/cli/src/lib/app/preview-provider.ts index e8a0b62..4476756 100644 --- a/packages/cli/src/lib/app/preview-provider.ts +++ b/packages/cli/src/lib/app/preview-provider.ts @@ -1,4 +1,5 @@ // biome-ignore-all lint/performance/noAwaitInLoops: API pagination and deployment lookup scans are intentionally sequential. +// biome-ignore-all lint/performance/useTopLevelRegex: Existing hostname normalization regexes are kept inline for readability. import path from "node:path"; import type { PortMapping, StreamRecord } from "@prisma/compute-sdk"; import { diff --git a/packages/cli/src/lib/project/local-pin.ts b/packages/cli/src/lib/project/local-pin.ts index b600835..8c8a47e 100644 --- a/packages/cli/src/lib/project/local-pin.ts +++ b/packages/cli/src/lib/project/local-pin.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/useTopLevelRegex: Existing local state text parsing regexes are kept inline for readability. import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; import path from "node:path"; diff --git a/packages/cli/src/lib/project/resolution.ts b/packages/cli/src/lib/project/resolution.ts index 0db7d9e..e2a30e0 100644 --- a/packages/cli/src/lib/project/resolution.ts +++ b/packages/cli/src/lib/project/resolution.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/useTopLevelRegex: Existing project-name validation regexes are kept inline for readability. import { readFile } from "node:fs/promises"; import path from "node:path"; diff --git a/packages/cli/src/lib/project/setup.ts b/packages/cli/src/lib/project/setup.ts index a235cd2..29c36a5 100644 --- a/packages/cli/src/lib/project/setup.ts +++ b/packages/cli/src/lib/project/setup.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/useTopLevelRegex: Existing setup formatting regexes are kept inline for readability. import { matchError, Result } from "better-result"; import { CliError, usageError } from "../../shell/errors"; import type { CommandContext } from "../../shell/runtime"; diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index b74e585..a653225 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/useTopLevelRegex: Existing executable detection regex is kept inline for readability. import { createRequire } from "node:module"; import process from "node:process"; diff --git a/packages/cli/src/shell/command-arguments.ts b/packages/cli/src/shell/command-arguments.ts index c0043d5..f10c639 100644 --- a/packages/cli/src/shell/command-arguments.ts +++ b/packages/cli/src/shell/command-arguments.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/useTopLevelRegex: Existing shell quoting regexes are kept inline for readability. export function formatCommandArgument(value: string): string { return /^[A-Za-z0-9._/-]+$/.test(value) && !value.startsWith("-") ? value diff --git a/packages/cli/src/shell/update-check.ts b/packages/cli/src/shell/update-check.ts index 63efee9..84e1914 100644 --- a/packages/cli/src/shell/update-check.ts +++ b/packages/cli/src/shell/update-check.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/useTopLevelRegex: Existing update-check parsing regexes are kept inline for readability. import { spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; diff --git a/packages/cli/tests/app-env.test.ts b/packages/cli/tests/app-env.test.ts index 337f78f..2b210c7 100644 --- a/packages/cli/tests/app-env.test.ts +++ b/packages/cli/tests/app-env.test.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/useTopLevelRegex: Test expectations keep regexes inline with assertions. import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; diff --git a/packages/cli/tests/shell.test.ts b/packages/cli/tests/shell.test.ts index 351289b..f470ae5 100644 --- a/packages/cli/tests/shell.test.ts +++ b/packages/cli/tests/shell.test.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/useTopLevelRegex: Test expectations keep regexes inline with assertions. import path from "node:path"; import stripAnsi from "strip-ansi"; import { describe, expect, it } from "vitest"; diff --git a/packages/cli/tests/version.test.ts b/packages/cli/tests/version.test.ts index 39e55b7..78732ab 100644 --- a/packages/cli/tests/version.test.ts +++ b/packages/cli/tests/version.test.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/performance/useTopLevelRegex: Test expectations keep regexes inline with assertions. import { createRequire } from "node:module"; import path from "node:path"; import process from "node:process"; diff --git a/scripts/resolve-cli-version.mjs b/scripts/resolve-cli-version.mjs index d56ffd8..cc06186 100644 --- a/scripts/resolve-cli-version.mjs +++ b/scripts/resolve-cli-version.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +// biome-ignore-all lint/performance/useTopLevelRegex: Release script regexes are kept inline for readability. import path from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/scripts/validate-skills.mjs b/scripts/validate-skills.mjs index 52e4d0b..37e5976 100644 --- a/scripts/validate-skills.mjs +++ b/scripts/validate-skills.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +// biome-ignore-all lint/performance/useTopLevelRegex: Skill validation regexes are kept inline with checks. import { readdirSync, readFileSync, statSync } from "node:fs"; import { createRequire } from "node:module"; From 5a61a241eb0be556e18706bedbc894e26ee32744 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:56:37 -0400 Subject: [PATCH 14/28] enable collapsed else if lint rule --- biome.jsonc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/biome.jsonc b/biome.jsonc index 07979b6..5103d2a 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -33,6 +33,9 @@ "noAwaitInLoops": "on", "noBarrelFile": "on", "useTopLevelRegex": "on" + }, + "style": { + "useCollapsedElseIf": "on" } // "complexity": { // }, @@ -41,7 +44,6 @@ // "performance": { // }, // "style": { - // "useCollapsedElseIf": "on", // "useCollapsedIf": "on", // "noNestedTernary": "on", // "noParameterAssign": "on" From 63f3c08edd2964fde37f9e99aff882ac4db59576 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:57:24 -0400 Subject: [PATCH 15/28] enable collapsed if lint rule --- biome.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 5103d2a..600bb3b 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -35,7 +35,8 @@ "useTopLevelRegex": "on" }, "style": { - "useCollapsedElseIf": "on" + "useCollapsedElseIf": "on", + "useCollapsedIf": "on" } // "complexity": { // }, @@ -44,7 +45,6 @@ // "performance": { // }, // "style": { - // "useCollapsedIf": "on", // "noNestedTernary": "on", // "noParameterAssign": "on" // }, From 47516a76846c019c8fe3948fda50da153f4b6dad Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:59:03 -0400 Subject: [PATCH 16/28] enable no nested ternary lint rule --- biome.jsonc | 4 ++-- packages/cli/src/controllers/app-env-file.ts | 1 + packages/cli/src/controllers/app.ts | 1 + packages/cli/src/lib/app/branch-database.ts | 1 + packages/cli/src/lib/app/preview-provider.ts | 1 + packages/cli/src/shell/ui.ts | 1 + 6 files changed, 7 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 600bb3b..a060afd 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -36,7 +36,8 @@ }, "style": { "useCollapsedElseIf": "on", - "useCollapsedIf": "on" + "useCollapsedIf": "on", + "noNestedTernary": "on" } // "complexity": { // }, @@ -45,7 +46,6 @@ // "performance": { // }, // "style": { - // "noNestedTernary": "on", // "noParameterAssign": "on" // }, // "suspicious": { diff --git a/packages/cli/src/controllers/app-env-file.ts b/packages/cli/src/controllers/app-env-file.ts index 14000f4..92d6af9 100644 --- a/packages/cli/src/controllers/app-env-file.ts +++ b/packages/cli/src/controllers/app-env-file.ts @@ -1,4 +1,5 @@ // 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 { type EnvScope, formatScopeLabel } from "../lib/app/env-config"; diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index 699a8a2..e5b36c8 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -1,5 +1,6 @@ // 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 type { PortMapping, StreamRecord } from "@prisma/compute-sdk"; diff --git a/packages/cli/src/lib/app/branch-database.ts b/packages/cli/src/lib/app/branch-database.ts index 621456e..6d333ce 100644 --- a/packages/cli/src/lib/app/branch-database.ts +++ b/packages/cli/src/lib/app/branch-database.ts @@ -1,5 +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"; diff --git a/packages/cli/src/lib/app/preview-provider.ts b/packages/cli/src/lib/app/preview-provider.ts index 4476756..c8b639c 100644 --- a/packages/cli/src/lib/app/preview-provider.ts +++ b/packages/cli/src/lib/app/preview-provider.ts @@ -1,5 +1,6 @@ // biome-ignore-all lint/performance/noAwaitInLoops: API pagination and deployment lookup scans are intentionally sequential. // biome-ignore-all lint/performance/useTopLevelRegex: Existing hostname normalization regexes are kept inline for readability. +// biome-ignore-all lint/style/noNestedTernary: Existing app resolution expression is intentionally compact. import path from "node:path"; import type { PortMapping, StreamRecord } from "@prisma/compute-sdk"; import { diff --git a/packages/cli/src/shell/ui.ts b/packages/cli/src/shell/ui.ts index 8aa50d3..ebd8be9 100644 --- a/packages/cli/src/shell/ui.ts +++ b/packages/cli/src/shell/ui.ts @@ -1,3 +1,4 @@ +// biome-ignore-all lint/style/noNestedTernary: Existing status symbol selection is intentionally compact. import { createColors } from "colorette"; import stringWidth from "string-width"; import stripAnsi from "strip-ansi"; From 5df9efbd1820ec15d3628e6faf3c8badd7135ea7 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 08:59:52 -0400 Subject: [PATCH 17/28] enable no parameter assign lint rule --- biome.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index a060afd..889abda 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -37,7 +37,8 @@ "style": { "useCollapsedElseIf": "on", "useCollapsedIf": "on", - "noNestedTernary": "on" + "noNestedTernary": "on", + "noParameterAssign": "on" } // "complexity": { // }, @@ -46,7 +47,6 @@ // "performance": { // }, // "style": { - // "noParameterAssign": "on" // }, // "suspicious": { // "noVar": "on", From 621b41f0f58cc64355515874bc9d8b0f210c3f96 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 09:00:40 -0400 Subject: [PATCH 18/28] enable no var lint rule --- biome.jsonc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/biome.jsonc b/biome.jsonc index 889abda..a159feb 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -39,6 +39,9 @@ "useCollapsedIf": "on", "noNestedTernary": "on", "noParameterAssign": "on" + }, + "suspicious": { + "noVar": "on" } // "complexity": { // }, @@ -49,7 +52,6 @@ // "style": { // }, // "suspicious": { - // "noVar": "on", // "useGuardForIn": "on", // "useStaticResponseMethods": "on" // } From 2d7f3c9095a35d67aabc6c764ece235a2a62b6ce Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 09:01:22 -0400 Subject: [PATCH 19/28] enable guarded for in lint rule --- biome.jsonc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index a159feb..a30e621 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -41,7 +41,8 @@ "noParameterAssign": "on" }, "suspicious": { - "noVar": "on" + "noVar": "on", + "useGuardForIn": "on" } // "complexity": { // }, @@ -52,7 +53,6 @@ // "style": { // }, // "suspicious": { - // "useGuardForIn": "on", // "useStaticResponseMethods": "on" // } } From 0eec80d40bfb2edb540467c08e7b2d95beff725c Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 09:03:14 -0400 Subject: [PATCH 20/28] enable static response methods lint rule --- biome.jsonc | 4 ++-- packages/cli/tests/update-check.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index a30e621..99a228b 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -42,7 +42,8 @@ }, "suspicious": { "noVar": "on", - "useGuardForIn": "on" + "useGuardForIn": "on", + "useStaticResponseMethods": "on" } // "complexity": { // }, @@ -53,7 +54,6 @@ // "style": { // }, // "suspicious": { - // "useStaticResponseMethods": "on" // } } }, diff --git a/packages/cli/tests/update-check.test.ts b/packages/cli/tests/update-check.test.ts index 9654c6d..ff87ddb 100644 --- a/packages/cli/tests/update-check.test.ts +++ b/packages/cli/tests/update-check.test.ts @@ -284,7 +284,7 @@ describe("automatic update check", () => { installedVersion: getCliVersion(), now: new Date("2026-01-02T00:00:00.000Z"), fetchImpl: async () => - new Response(JSON.stringify({ "dist-tags": { latest: "9.8.7" } })), + Response.json({ "dist-tags": { latest: "9.8.7" } }), }); expect(await readUpdateCheckState(updateCheckDir)).toMatchObject({ @@ -310,7 +310,7 @@ describe("automatic update check", () => { installedVersion: getCliVersion(), now: new Date("2026-01-02T00:00:00.000Z"), fetchImpl: async () => - new Response(JSON.stringify({ "dist-tags": { latest: "9.8.7" } })), + Response.json({ "dist-tags": { latest: "9.8.7" } }), }); expect(await readUpdateCheckState(updateCheckDir)).toMatchObject({ From d6dac32fd64c3a2c79300aaf899d859a1828424f Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 09:04:20 -0400 Subject: [PATCH 21/28] remove empty biome rule comments --- biome.jsonc | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 99a228b..9d7b8c0 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -45,16 +45,6 @@ "useGuardForIn": "on", "useStaticResponseMethods": "on" } - // "complexity": { - // }, - // "nursery": { - // }, - // "performance": { - // }, - // "style": { - // }, - // "suspicious": { - // } } }, "javascript": { From aa69bbbd9f11be5bc9d70e664acf27d0067fccba Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 09:18:34 -0400 Subject: [PATCH 22/28] fix mask value regex backtracking --- packages/cli/src/shell/ui.ts | 50 +++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/shell/ui.ts b/packages/cli/src/shell/ui.ts index ebd8be9..7992b8d 100644 --- a/packages/cli/src/shell/ui.ts +++ b/packages/cli/src/shell/ui.ts @@ -7,6 +7,8 @@ import wrapAnsi from "wrap-ansi"; import type { GlobalFlags } from "./global-flags"; import type { CliRuntime } from "./runtime"; +const URL_CREDENTIALS_PATTERN = /:\/\/[^:@/\s]+:[^@/\s]+@/g; + export interface ShellUi { isTTY: boolean; colorEnabled: boolean; @@ -214,9 +216,51 @@ export function padDisplay(text: string, width: number): string { } export function maskValue(value: string): string { - return value - .replace(/([A-Za-z0-9._%+-]{1,})(?=@)/g, "****") - .replace(/:\/\/[^:@/\s]+:[^@/\s]+@/g, "://****:****@"); + return maskEmailLocalParts(value).replace( + URL_CREDENTIALS_PATTERN, + "://****:****@", + ); +} + +function maskEmailLocalParts(value: string): string { + let masked = ""; + let segmentStart = 0; + + for (let index = 0; index < value.length; index += 1) { + if (value[index] !== "@") { + continue; + } + + let localStart = index; + while ( + localStart > segmentStart && + isEmailLocalPartChar(value[localStart - 1]) + ) { + localStart -= 1; + } + + if (localStart === index) { + continue; + } + + masked += `${value.slice(segmentStart, localStart)}****@`; + segmentStart = index + 1; + } + + return masked + value.slice(segmentStart); +} + +function isEmailLocalPartChar(char: string): boolean { + return ( + (char >= "A" && char <= "Z") || + (char >= "a" && char <= "z") || + (char >= "0" && char <= "9") || + char === "." || + char === "_" || + char === "%" || + char === "+" || + char === "-" + ); } function resolveColorEnabled( From 5a3562581f3a0a1d91b7f8dc168caf6f56dd7f87 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 09:24:08 -0400 Subject: [PATCH 23/28] fix constructable test mocks --- packages/cli/tests/app-bun-compat.test.ts | 8 +++++--- packages/cli/tests/app-provider.test.ts | 6 +++++- packages/cli/tests/auth-ops.test.ts | 4 +++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/cli/tests/app-bun-compat.test.ts b/packages/cli/tests/app-bun-compat.test.ts index 02dcd53..560a7ce 100644 --- a/packages/cli/tests/app-bun-compat.test.ts +++ b/packages/cli/tests/app-bun-compat.test.ts @@ -17,9 +17,11 @@ function mockBuildStrategy( execute: vi.fn(), }), ) { - return vi - .fn() - .mockImplementation((options: object) => createInstance(options)); + return vi.fn().mockImplementation(function BuildStrategyMock( + options: object, + ) { + return createInstance(options); + }); } describe("bun compatibility", () => { diff --git a/packages/cli/tests/app-provider.test.ts b/packages/cli/tests/app-provider.test.ts index 14e1a73..199e34d 100644 --- a/packages/cli/tests/app-provider.test.ts +++ b/packages/cli/tests/app-provider.test.ts @@ -10,7 +10,11 @@ afterEach(() => { }); function mockPreviewBuildStrategy() { - return vi.fn().mockImplementation((options: object) => ({ options })); + return vi.fn().mockImplementation(function PreviewBuildStrategyMock( + options: object, + ) { + return { options }; + }); } describe("preview app provider", () => { diff --git a/packages/cli/tests/auth-ops.test.ts b/packages/cli/tests/auth-ops.test.ts index f353e55..f0470db 100644 --- a/packages/cli/tests/auth-ops.test.ts +++ b/packages/cli/tests/auth-ops.test.ts @@ -15,7 +15,9 @@ function encodeJwt(claims: Record): string { } function mockFileTokenStorage(getTokens: ReturnType) { - return vi.fn().mockImplementation(() => ({ getTokens })); + return vi.fn().mockImplementation(function FileTokenStorageMock() { + return { getTokens }; + }); } describe("readAuthState", () => { From 53f61afea55681677bd4830abf41256f916c4564 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 09:33:20 -0400 Subject: [PATCH 24/28] Add PR quality workflow --- .github/workflows/AGENTS.md | 3 ++ .github/workflows/pr-quality.yml | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 .github/workflows/AGENTS.md create mode 100644 .github/workflows/pr-quality.yml 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..a04f639 --- /dev/null +++ b/.github/workflows/pr-quality.yml @@ -0,0 +1,75 @@ +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 + + - 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 + + - 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 + + - 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 From 2bd3a599e0255a8cea248bf507477f2de704e7ac Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 09:35:12 -0400 Subject: [PATCH 25/28] Set Node 22.12.0 as minimum version --- .node-version | 1 + package.json | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 .node-version 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/package.json b/package.json index 8036952..6e0f1e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "prisma-cli", "private": true, + "engines": { + "node": ">=22.12.0" + }, "packageManager": "pnpm@10.30.0", "scripts": { "build:cli": "pnpm --filter @prisma/cli build", From 91c4f3faedf5aaeb1e4964bcf5217d0f504567b4 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 09:54:37 -0400 Subject: [PATCH 26/28] address biome review feedback --- .github/workflows/pr-quality.yml | 6 ++++++ examples/next-smoke/app/globals.css | 2 +- package.json | 3 ++- packages/cli/src/controllers/project.ts | 5 ++++- packages/cli/src/presenters/database.ts | 2 +- packages/cli/src/shell/ui.ts | 19 +++++++++++++++++-- packages/cli/tests/shell.test.ts | 11 +++++++++++ 7 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-quality.yml b/.github/workflows/pr-quality.yml index a04f639..93ab484 100644 --- a/.github/workflows/pr-quality.yml +++ b/.github/workflows/pr-quality.yml @@ -21,6 +21,8 @@ jobs: 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 @@ -44,6 +46,8 @@ jobs: 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 @@ -58,6 +62,8 @@ jobs: 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 diff --git a/examples/next-smoke/app/globals.css b/examples/next-smoke/app/globals.css index b294c93..7b2f897 100644 --- a/examples/next-smoke/app/globals.css +++ b/examples/next-smoke/app/globals.css @@ -40,7 +40,7 @@ body { code { font-family: - "SFMono-Regular", ui-monospace, "Cascadia Mono", "Segoe UI Mono", Menlo, + SFMono-Regular, ui-monospace, "Cascadia Mono", "Segoe UI Mono", Menlo, Consolas, monospace; background: rgba(42, 44, 39, 0.06); border-radius: 0.5rem; diff --git a/package.json b/package.json index 6e0f1e2..e1a2a58 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "build:cli": "pnpm --filter @prisma/cli build", "build:compute": "pnpm --filter @prisma/compute build", "format": "biome format . --write", - "lint": "biome check . --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", diff --git a/packages/cli/src/controllers/project.ts b/packages/cli/src/controllers/project.ts index 82d9803..79ca714 100644 --- a/packages/cli/src/controllers/project.ts +++ b/packages/cli/src/controllers/project.ts @@ -1,4 +1,3 @@ -// biome-ignore-all lint/performance/noAwaitInLoops: SCM pagination, polling, and ordered fallback checks are intentionally sequential. import type { ManagementApiClient } from "@prisma/management-api-sdk"; import { matchError } from "better-result"; import open from "open"; @@ -1129,6 +1128,7 @@ async function findRepositoryInInstallations( continue; } + // 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, @@ -1179,6 +1179,7 @@ async function waitForInstalledRepository( while (Date.now() <= deadline) { context.runtime.signal.throwIfAborted(); + // biome-ignore lint/performance/noAwaitInLoops: Polling intentionally waits for each remote inspection before sleeping or retrying. const installations = await listScmInstallations( api, workspaceId, @@ -1270,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: { @@ -1312,6 +1314,7 @@ async function findRepositoryInInstallation( 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/{installationId}/repositories", { diff --git a/packages/cli/src/presenters/database.ts b/packages/cli/src/presenters/database.ts index fb6c18d..025d051 100644 --- a/packages/cli/src/presenters/database.ts +++ b/packages/cli/src/presenters/database.ts @@ -327,7 +327,7 @@ export function renderDatabaseConnectionRemove( export function serializeDatabaseConnectionRemove( result: DatabaseConnectionRemoveResult, ) { - return result; + return { connection: result.connection }; } function formatStatus(database: DatabaseSummary): string { diff --git a/packages/cli/src/shell/ui.ts b/packages/cli/src/shell/ui.ts index 7992b8d..52425be 100644 --- a/packages/cli/src/shell/ui.ts +++ b/packages/cli/src/shell/ui.ts @@ -255,11 +255,26 @@ function isEmailLocalPartChar(char: string): boolean { (char >= "A" && char <= "Z") || (char >= "a" && char <= "z") || (char >= "0" && char <= "9") || + char === "!" || + char === "#" || + char === "$" || char === "." || - char === "_" || + char === "&" || + char === "'" || + char === "*" || char === "%" || char === "+" || - char === "-" + char === "-" || + char === "/" || + char === "=" || + char === "?" || + char === "^" || + char === "_" || + char === "`" || + char === "{" || + char === "|" || + char === "}" || + char === "~" ); } diff --git a/packages/cli/tests/shell.test.ts b/packages/cli/tests/shell.test.ts index f470ae5..574f57e 100644 --- a/packages/cli/tests/shell.test.ts +++ b/packages/cli/tests/shell.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; import { formatCommandArgument } from "../src/shell/command-arguments"; import { formatUnexpectedError } from "../src/shell/output"; +import { maskValue } from "../src/shell/ui"; import { createTempCwd, executeCli } from "./helpers"; const fixturePath = path.resolve("fixtures/mock-api.json"); @@ -32,6 +33,16 @@ describe("shell behavior", () => { expect(formatUnexpectedError(error, true)).toContain("at explode"); }); + it("masks sensitive email local parts with RFC-style characters", () => { + expect(maskValue("user.name+tag@example.com")).toBe("****@example.com"); + expect(maskValue("customer!#$%&'*+-/=?^_`{|}~@example.com")).toBe( + "****@example.com", + ); + expect(maskValue("postgres://user:secret@db.example.com/app")).toBe( + "postgres://****:****@db.example.com/app", + ); + }); + it("renders root help with workflow groups", async () => { const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); From 4c12f7e763e2c371829583ca77ece8072b8f73dd Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 10:17:15 -0400 Subject: [PATCH 27/28] trigger ci From efea28eba49e9fa552a269e71eff2f16a9037787 Mon Sep 17 00:00:00 2001 From: Tyler Benfield Date: Fri, 12 Jun 2026 10:26:34 -0400 Subject: [PATCH 28/28] fix ci typecheck --- packages/cli/src/controllers/app.ts | 5 ++- packages/cli/src/lib/app/branch-database.ts | 40 +++++++++++++++++--- packages/cli/tests/helpers.ts | 3 +- packages/cli/tests/project-real-mode.test.ts | 16 ++++++-- packages/compute/tests/scale-to-zero.test.ts | 4 +- scripts/prepare-cli-publish.d.mts | 5 +++ scripts/resolve-cli-version.d.mts | 13 +++++++ 7 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 scripts/prepare-cli-publish.d.mts create mode 100644 scripts/resolve-cli-version.d.mts diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index e5b36c8..9b6f3e4 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -4019,7 +4019,10 @@ function appDeployFailedError( }); } -function appBuildFailedError(why: string, debug: string | undefined): CliError { +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.' diff --git a/packages/cli/src/lib/app/branch-database.ts b/packages/cli/src/lib/app/branch-database.ts index 6d333ce..779433b 100644 --- a/packages/cli/src/lib/app/branch-database.ts +++ b/packages/cli/src/lib/app/branch-database.ts @@ -194,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; @@ -341,16 +349,26 @@ async function selectPrismaOrmSchema( }; } +function selectPrismaNextConfig( + cwd: string, + candidates: ClassifiedPrismaNextConfig[], + mode: "supported", +): SupportedPrismaNextConfig | null; +function selectPrismaNextConfig( + cwd: string, + candidates: ClassifiedPrismaNextConfig[], + mode: "unsupported", +): UnsupportedPrismaNextConfig | null; function selectPrismaNextConfig( cwd: string, candidates: ClassifiedPrismaNextConfig[], mode: "supported" | "unsupported", ): ClassifiedPrismaNextConfig | null { - const matches = candidates.filter((candidate) => { - const isSupported = - candidate.target === "postgresql" || candidate.target === "unknown"; - return mode === "supported" ? isSupported : !isSupported; - }); + const matches = candidates.filter( + mode === "supported" + ? isSupportedPrismaNextConfig + : isUnsupportedPrismaNextConfig, + ); return ( sortByPreferredRelativePath( @@ -367,6 +385,18 @@ function selectPrismaNextConfig( ); } +function isSupportedPrismaNextConfig( + candidate: ClassifiedPrismaNextConfig, +): candidate is SupportedPrismaNextConfig { + return candidate.target === "postgresql" || candidate.target === "unknown"; +} + +function isUnsupportedPrismaNextConfig( + candidate: ClassifiedPrismaNextConfig, +): candidate is UnsupportedPrismaNextConfig { + return !isSupportedPrismaNextConfig(candidate); +} + function sortByPreferredRelativePath( cwd: string, candidates: string[], diff --git a/packages/cli/tests/helpers.ts b/packages/cli/tests/helpers.ts index cb8e6fa..a86f5e0 100644 --- a/packages/cli/tests/helpers.ts +++ b/packages/cli/tests/helpers.ts @@ -62,13 +62,14 @@ export async function executeCli(options: { stderr.columns = 80; stdout.rows = 24; stderr.rows = 24; + const cwd = options.cwd ?? process.cwd(); const stdin = new CaptureInput(); stdin.isTTY = options.isTTY ?? false; const env = createTestEnv(options.env, options.preserveCI); const runtime: CliRuntime = { argv: options.argv, - cwd: options.cwd, + cwd, env, signal: new AbortController().signal, fixturePath: options.fixturePath, diff --git a/packages/cli/tests/project-real-mode.test.ts b/packages/cli/tests/project-real-mode.test.ts index 5d01ad1..f47dbfd 100644 --- a/packages/cli/tests/project-real-mode.test.ts +++ b/packages/cli/tests/project-real-mode.test.ts @@ -1,6 +1,14 @@ import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, type Mock, vi } from "vitest"; + +type ApiGetMock = Mock< + ( + pathName: string, + request?: { params?: { query?: Record } }, + ) => unknown +>; +type ApiMutationMock = Mock<(pathName: string, request?: unknown) => unknown>; afterEach(() => { vi.doUnmock("../src/lib/auth/auth-ops"); @@ -26,9 +34,9 @@ function mockAuthState() { function mockClient( extra: Partial<{ - GET: ReturnType; - POST: ReturnType; - DELETE: ReturnType; + GET: ApiGetMock; + POST: ApiMutationMock; + DELETE: ApiMutationMock; }> = {}, ) { return { diff --git a/packages/compute/tests/scale-to-zero.test.ts b/packages/compute/tests/scale-to-zero.test.ts index ade4039..162023d 100644 --- a/packages/compute/tests/scale-to-zero.test.ts +++ b/packages/compute/tests/scale-to-zero.test.ts @@ -97,7 +97,9 @@ describe("scale-to-zero guard", () => { controller.abort(); expect(await readSignals(file)).toBe("+-"); - expect(resolvePromise).toBeDefined(); + if (!resolvePromise) { + throw new Error("Expected promise resolver to be captured"); + } resolvePromise("done"); await expect(promise).resolves.toBe("done"); await Promise.resolve(); diff --git a/scripts/prepare-cli-publish.d.mts b/scripts/prepare-cli-publish.d.mts new file mode 100644 index 0000000..75cad70 --- /dev/null +++ b/scripts/prepare-cli-publish.d.mts @@ -0,0 +1,5 @@ +export declare function stageCliPublishPackage(options?: { + sourceDir?: string; + outputDir?: string; + publishVersion?: string; +}): Promise; diff --git a/scripts/resolve-cli-version.d.mts b/scripts/resolve-cli-version.d.mts new file mode 100644 index 0000000..f764041 --- /dev/null +++ b/scripts/resolve-cli-version.d.mts @@ -0,0 +1,13 @@ +export declare const CLI_RELEASE_BASE_VERSION: string; + +export declare function resolveDevVersion(options: { + runNumber?: string | number | null; + runAttempt?: string | number | null; +}): string; + +export declare function resolvePrVersion(options: { + prNumber?: string | number | null; + sha?: string | null; +}): string; + +export declare function resolveNextBetaVersion(latest?: string | null): string;