diff --git a/AGENTS.md b/AGENTS.md index 12d1d4cf..98b96365 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,8 @@ That file also holds `AWAITING_COVERAGE`: commands that were already mounted whe Run the suite with `pnpm --filter @prisma/cli test:e2e`. It needs `PRISMA_E2E_SERVICE_TOKEN` (and optionally `PRISMA_E2E_WORKSPACE_ID`) for a workspace you are willing to see resources created and deleted in; without them the suite skips. CI sets `PRISMA_E2E_REQUIRED=1`, which turns a missing credential into a failure rather than a silent skip. +Unit tests may still mock, and should — error paths and edge cases belong there. Two rules keep those mocks honest. Give fixtures the id shapes the API really uses: `wksp_`-prefixed workspace ids in API responses, the bare form in credential claims and stored sessions, and the `proj_` / `db_` / `bkt_` prefixes on resources. And never write both sides of a comparison from one constant — if a test supplies the credential's workspace id and the API's, they must differ exactly as they differ in production. Where a fake API server is easier than mocking a client, `packages/cli/tests/helpers/fake-management-api.ts` starts one. + Why this rule exists: `prisma-v8 project list` reported "No projects found." and exited 0 for a workspace holding 15 projects, and every project-scoped command was broken with it. The unit suite covered that command thoroughly and passed throughout, because its fixtures supplied both sides of every comparison — the credential's workspace id and the API's were the same hand-written string, while the real API returns a `wksp_` prefix that the credential does not carry. A test that writes both sides of a comparison can only confirm what its author already believed. Mocks are still the right tool for error paths and edge cases; they cannot tell you what the API actually returns. ## Pre-Commit Verification diff --git a/packages/cli/e2e/agent.e2e.ts b/packages/cli/e2e/agent.e2e.ts new file mode 100644 index 00000000..563bcb06 --- /dev/null +++ b/packages/cli/e2e/agent.e2e.ts @@ -0,0 +1,85 @@ +/** + * The agent commands install and report Prisma's skills for local + * coding agents. They touch no management API — they run the `skills` + * CLI and write files into the working directory — but they ship in the + * binary, so they get the same real happy path as everything else. + * + * Each runs in a throwaway working directory, so the files they write + * belong to the run and go with it. + */ +import { existsSync } from "node:fs"; +import path from "node:path"; + +import { beforeAll, expect, it } from "vitest"; + +import { describeCommand, session } from "./suite"; + +interface StatusResult { + readonly skillsInstalled: boolean; + readonly skillsLockInstalled: boolean; + readonly skillsLockPath: string; + readonly statusScope: string; +} + +interface OperationResult { + readonly operation: string; + readonly skills: { readonly status: string }; +} + +/** Shared so `status` can be asked before and after `install`, which is + * what shows the install did something. */ +let workdir: string; +let installedBefore: StatusResult | undefined; + +beforeAll(async () => { + const cli = await session(); + workdir = await cli.workdir(); + installedBefore = (await cli.run(["agent", "status"], { cwd: workdir })) + .envelope.result as StatusResult; +}); + +describeCommand("agent status", () => { + it("reports nothing installed in a fresh directory", async () => { + expect(installedBefore?.statusScope).toBe("project"); + expect(installedBefore?.skillsInstalled).toBe(false); + expect(installedBefore?.skillsLockInstalled).toBe(false); + expect(installedBefore?.skillsLockPath).toBe("skills-lock.json"); + }); +}); + +describeCommand("agent install", () => { + it("installs the skills and writes the lock file", async () => { + const cli = await session(); + const run = await cli.run(["agent", "install"], { cwd: workdir }); + const result = run.envelope.result as OperationResult; + + expect(result.operation).toBe("install"); + expect(result.skills.status).toBe("installed"); + // The command's own answer is not the whole story: the lock file it + // claims to write has to be there. + expect(existsSync(path.join(workdir, "skills-lock.json"))).toBe(true); + + const after = (await cli.run(["agent", "status"], { cwd: workdir })) + .envelope.result as StatusResult; + expect(after.skillsLockInstalled).toBe(true); + expect(after.skillsInstalled).toBe(true); + }); +}); + +describeCommand("agent update", () => { + it("updates the skills already installed", async () => { + const cli = await session(); + // Its own directory and its own install: depending on the block + // above would make this pass or fail on test order, and a focused + // run would find an empty directory. + const cwd = await cli.workdir(); + await cli.run(["agent", "install"], { cwd }); + + const run = await cli.run(["agent", "update"], { cwd }); + const result = run.envelope.result as OperationResult; + + expect(result.operation).toBe("update"); + expect(result.skills.status).toBe("installed"); + expect(existsSync(path.join(cwd, "skills-lock.json"))).toBe(true); + }); +}); diff --git a/packages/cli/fixtures/mock-api.json b/packages/cli/fixtures/mock-api.json deleted file mode 100644 index 4d56507a..00000000 --- a/packages/cli/fixtures/mock-api.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "providers": [ - { "id": "github", "name": "GitHub" }, - { "id": "google", "name": "Google" } - ], - "users": [ - { - "id": "usr_123", - "name": "Alice Example", - "email": "alice@example.com", - "providerIds": ["github", "google"] - }, - { - "id": "usr_456", - "name": "Bob Example", - "email": "bob@example.com", - "providerIds": ["github"] - } - ], - "workspaces": [ - { - "id": "ws_123", - "name": "Acme Inc", - "slug": "acme" - }, - { - "id": "ws_456", - "name": "Prisma Labs", - "slug": "prisma" - } - ], - "memberships": [ - { - "userId": "usr_123", - "workspaceId": "ws_123" - }, - { - "userId": "usr_123", - "workspaceId": "ws_456" - }, - { - "userId": "usr_456", - "workspaceId": "ws_123" - } - ], - "projects": [ - { - "id": "proj_123", - "name": "Acme Dashboard", - "slug": "acme-dashboard", - "url": "https://prisma.build/acme/acme-dashboard", - "workspaceId": "ws_123" - }, - { - "id": "proj_456", - "name": "Billing API", - "slug": "billing-api", - "url": "https://prisma.build/acme/billing-api", - "workspaceId": "ws_123" - }, - { - "id": "proj_789", - "name": "Website", - "slug": "website", - "url": "https://prisma.build/prisma/website", - "workspaceId": "ws_456" - }, - { - "id": "proj_999", - "name": "Sandbox", - "slug": "sandbox", - "url": "https://prisma.build/acme/sandbox", - "workspaceId": "ws_123" - } - ], - "branches": [ - { - "id": "br_123", - "projectId": "proj_123", - "name": "preview", - "role": "preview", - "currentDeploymentId": "dep_123" - }, - { - "id": "br_234", - "projectId": "proj_123", - "name": "pr-123", - "role": "preview", - "currentDeploymentId": "dep_234" - }, - { - "id": "br_345", - "projectId": "proj_123", - "name": "staging", - "role": "preview", - "currentDeploymentId": null - }, - { - "id": "br_456", - "projectId": "proj_123", - "name": "production", - "role": "production", - "currentDeploymentId": "dep_456" - }, - { - "id": "br_789", - "projectId": "proj_456", - "name": "preview", - "role": "preview", - "currentDeploymentId": "dep_789" - } - ], - "deployments": [ - { - "id": "dep_123", - "projectId": "proj_123", - "branch": "preview", - "status": "ready", - "url": "https://preview.acme-dashboard.prisma.app" - }, - { - "id": "dep_234", - "projectId": "proj_123", - "branch": "pr-123", - "status": "ready", - "url": "https://pr-123.acme-dashboard.prisma.app" - }, - { - "id": "dep_456", - "projectId": "proj_123", - "branch": "production", - "status": "ready", - "url": "https://acme-dashboard.prisma.app" - }, - { - "id": "dep_789", - "projectId": "proj_456", - "branch": "preview", - "status": "ready", - "url": "https://preview.billing-api.prisma.app" - } - ], - "databases": [ - { - "id": "db_123", - "projectId": "proj_123", - "branchId": "br_123", - "branchName": "preview", - "name": "acme-preview", - "region": "eu-central-1", - "status": "ready", - "isDefault": true, - "createdAt": "2026-06-01T00:00:00.000Z" - }, - { - "id": "db_456", - "projectId": "proj_123", - "branchId": "br_456", - "branchName": "production", - "name": "acme-production", - "region": "us-east-1", - "status": "ready", - "isDefault": false, - "createdAt": "2026-06-02T00:00:00.000Z" - }, - { - "id": "db_789", - "projectId": "proj_456", - "branchId": "br_789", - "branchName": "preview", - "name": "billing-preview", - "region": "eu-central-1", - "status": "ready", - "isDefault": false, - "createdAt": "2026-06-03T00:00:00.000Z" - } - ], - "databaseConnections": [ - { - "id": "conn_123", - "databaseId": "db_123", - "name": "primary", - "createdAt": "2026-06-01T00:00:00.000Z", - "connectionString": "postgresql://secret-preview.example.prisma.io/postgres" - }, - { - "id": "conn_456", - "databaseId": "db_456", - "name": "primary", - "createdAt": "2026-06-02T00:00:00.000Z", - "connectionString": "postgresql://secret-production.example.prisma.io/postgres" - } - ], - "databaseBackups": [ - { - "id": "bkp_101", - "databaseId": "db_123", - "backupType": "full", - "status": "completed", - "size": 1048576, - "createdAt": "2026-06-20T00:00:00.000Z" - }, - { - "id": "bkp_102", - "databaseId": "db_123", - "backupType": "incremental", - "status": "completed", - "createdAt": "2026-06-21T00:00:00.000Z" - }, - { - "id": "bkp_201", - "databaseId": "db_456", - "backupType": "full", - "status": "completed", - "size": 5242880, - "createdAt": "2026-06-22T00:00:00.000Z" - } - ], - "databaseBackupRetentionDays": 35, - "databaseUsage": [ - { - "databaseId": "db_123", - "period": { - "start": "2026-06-01T00:00:00.000Z", - "end": "2026-06-30T23:59:59.999Z" - }, - "metrics": { - "operations": { "used": 12500, "unit": "ops" }, - "storage": { "used": 1.25, "unit": "GiB" } - }, - "generatedAt": "2026-07-01T00:00:00.000Z" - } - ], - "buckets": [ - { - "id": "bkt_123", - "projectId": "proj_123", - "branchId": "br_123", - "name": "acme-preview-store", - "status": "ready", - "createdAt": "2026-06-01T00:00:00.000Z" - }, - { - "id": "bkt_456", - "projectId": "proj_123", - "branchId": "br_456", - "name": "acme-production-store", - "status": "ready", - "createdAt": "2026-06-02T00:00:00.000Z" - } - ], - "bucketKeys": [ - { - "id": "bkey_123", - "bucketId": "bkt_123", - "name": "primary", - "role": "read_write", - "valueHint": "AKIABKT123...", - "createdAt": "2026-06-01T00:00:00.000Z" - } - ] -} diff --git a/packages/cli/src/adapters/mock-api.ts b/packages/cli/src/adapters/mock-api.ts deleted file mode 100644 index 0d0bd357..00000000 --- a/packages/cli/src/adapters/mock-api.ts +++ /dev/null @@ -1,696 +0,0 @@ -import { readFile } from "node:fs/promises"; - -import type { AuthProviderId } from "../types/auth"; - -interface ProviderRecord { - id: AuthProviderId; - name: string; -} - -interface UserRecord { - id: string; - name: string; - email: string; - providerIds: AuthProviderId[]; -} - -interface WorkspaceRecord { - id: string; - name: string; - slug: string; -} - -interface MembershipRecord { - userId: string; - workspaceId: string; -} - -interface ProjectRecord { - id: string; - name: string; - slug: string; - url?: string; - workspaceId: string; -} - -interface BranchRecord { - id: string; - projectId: string; - name: string; - role: "production" | "preview"; - currentDeploymentId: string | null; -} - -interface DeploymentRecord { - id: string; - projectId: string; - branch: string; - status: string; - url: string | null; -} - -interface DatabaseRecord { - id: string; - projectId: string; - branchId: string | null; - branchName: string | null; - name: string; - region: string | null; - status: string | null; - isDefault: boolean | null; - createdAt: string | null; -} - -interface DatabaseConnectionRecord { - id: string; - databaseId: string; - name: string; - createdAt: string | null; - connectionString?: string; -} - -interface DatabaseBackupRecord { - id: string; - databaseId: string; - backupType: string; - status: string; - size?: number; - createdAt: string; -} - -interface DatabaseUsageRecord { - databaseId: string; - period: { start: string; end: string }; - metrics: { - operations: { used: number; unit: string }; - storage: { used: number; unit: string }; - }; - generatedAt: string; -} - -interface BucketRecord { - id: string; - projectId: string; - branchId: string | null; - name: string; - status: string; - createdAt: string; -} - -interface BucketKeyRecord { - id: string; - bucketId: string; - name: string; - role: "read" | "read_write"; - valueHint: string; - createdAt: string; - secretAccessKey?: string; - accessKeyId?: string; - endpoint?: string; - bucketName?: string; -} - -interface MockApiData { - providers: ProviderRecord[]; - users: UserRecord[]; - workspaces: WorkspaceRecord[]; - memberships: MembershipRecord[]; - projects: ProjectRecord[]; - branches: BranchRecord[]; - deployments: DeploymentRecord[]; - databases?: DatabaseRecord[]; - databaseConnections?: DatabaseConnectionRecord[]; - databaseBackups?: DatabaseBackupRecord[]; - databaseUsage?: DatabaseUsageRecord[]; - databaseBackupRetentionDays?: number; - buckets?: BucketRecord[]; - bucketKeys?: BucketKeyRecord[]; -} - -export class MockApi { - private readonly data: MockApiData; - - private constructor(data: MockApiData) { - this.data = data; - } - - 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); - } - - listProviders(): ProviderRecord[] { - return this.data.providers; - } - - getProvider(providerId: string): ProviderRecord | undefined { - return this.data.providers.find((provider) => provider.id === providerId); - } - - listUsersForProvider(providerId: AuthProviderId): UserRecord[] { - 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, - ); - } - - listUserWorkspaces(userId: string): WorkspaceRecord[] { - const workspaceIds = this.data.memberships - .filter((membership) => membership.userId === userId) - .map((membership) => membership.workspaceId); - - return this.data.workspaces.filter((workspace) => - workspaceIds.includes(workspace.id), - ); - } - - listWorkspaces(): WorkspaceRecord[] { - return this.data.workspaces; - } - - getWorkspace(workspaceId: string): WorkspaceRecord | undefined { - 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, - ); - } - - listProjectsForWorkspace(workspaceId: string): ProjectRecord[] { - 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, - ); - } - - renameProject(projectId: string, name: string): ProjectRecord | undefined { - const project = this.getProject(projectId); - if (!project) { - return undefined; - } - - project.name = name; - return project; - } - - removeProject( - projectId: string, - ): - | { outcome: "removed"; project: ProjectRecord } - | { outcome: "not-found" } - | { outcome: "blocked" } { - const project = this.getProject(projectId); - if (!project) { - return { outcome: "not-found" }; - } - - // Mirrors the platform rule: removal is blocked while the project still - // has active deployments. - const hasDeployments = this.data.deployments.some( - (deployment) => deployment.projectId === projectId, - ); - if (hasDeployments) { - return { outcome: "blocked" }; - } - - const removedDatabaseIds = new Set( - (this.data.databases ?? []) - .filter((database) => database.projectId === projectId) - .map((database) => database.id), - ); - - this.data.projects = this.data.projects.filter( - (candidate) => candidate.id !== projectId, - ); - this.data.branches = this.data.branches.filter( - (branch) => branch.projectId !== projectId, - ); - this.data.databases = (this.data.databases ?? []).filter( - (database) => database.projectId !== projectId, - ); - this.data.databaseConnections = ( - this.data.databaseConnections ?? [] - ).filter((connection) => !removedDatabaseIds.has(connection.databaseId)); - return { outcome: "removed", project }; - } - - transferProject( - projectId: string, - targetWorkspaceId: string, - ): - | { outcome: "transferred"; project: ProjectRecord } - | { outcome: "not-found" } - | { outcome: "workspace-not-found" } { - const project = this.getProject(projectId); - if (!project) { - return { outcome: "not-found" }; - } - if (!this.getWorkspace(targetWorkspaceId)) { - return { outcome: "workspace-not-found" }; - } - - project.workspaceId = targetWorkspaceId; - return { outcome: "transferred", project }; - } - - listBranchesForProject(projectId: string): BranchRecord[] { - 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, - ); - } - - getDeployment(deploymentId: string): DeploymentRecord | undefined { - 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), - ); - } - - getDatabase(databaseId: string): DatabaseRecord | undefined { - return (this.data.databases ?? []).find( - (database) => database.id === databaseId, - ); - } - - createDatabase(input: { - projectId: string; - name: string; - branchName?: string; - region?: 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, - branchName: input.branchName ?? null, - name: input.name, - region: input.region ?? null, - status: "ready", - isDefault: false, - createdAt: "2026-06-09T00:00:00.000Z", - }; - const connectionString = `postgresql://${database.id}.example.prisma.io/postgres`; - const connection: DatabaseConnectionRecord = { - id: `conn_${this.data.databaseConnections.length + 1_000}`, - databaseId: database.id, - name: "primary", - createdAt: "2026-06-09T00:00:00.000Z", - connectionString, - }; - - this.data.databases.push(database); - this.data.databaseConnections.push(connection); - - return { database, connection, connectionString }; - } - - removeDatabase(databaseId: string): DatabaseRecord | undefined { - this.data.databases ??= []; - this.data.databaseConnections ??= []; - const database = this.getDatabase(databaseId); - if (!database) { - return undefined; - } - - 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, - ); - } - - 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 { - const database = this.getDatabase(input.databaseId); - if (!database) { - return undefined; - } - - this.data.databaseConnections ??= []; - const connectionString = `postgresql://${input.databaseId}-${this.data.databaseConnections.length + 1}.example.prisma.io/postgres`; - const connection: DatabaseConnectionRecord = { - id: `conn_${this.data.databaseConnections.length + 1_000}`, - databaseId: input.databaseId, - name: input.name, - createdAt: "2026-06-09T00:00:00.000Z", - connectionString, - }; - this.data.databaseConnections.push(connection); - return { connection, connectionString }; - } - - 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, - ); - return connection; - } - - getDatabaseUsage( - databaseId: string, - period?: { from?: string; to?: string }, - ): { - period: { start: string; end: string }; - metrics: { - operations: { used: number; unit: string }; - storage: { used: number; unit: string }; - }; - generatedAt: string; - } { - const usage = (this.data.databaseUsage ?? []).find( - (record) => record.databaseId === databaseId, - ); - const defaults = usage ?? { - databaseId, - period: { - start: "2026-06-01T00:00:00.000Z", - end: "2026-06-30T23:59:59.999Z", - }, - metrics: { - operations: { used: 0, unit: "ops" }, - storage: { used: 0, unit: "GiB" }, - }, - generatedAt: "2026-07-01T00:00:00.000Z", - }; - - return { - period: { - start: period?.from ?? defaults.period.start, - end: period?.to ?? defaults.period.end, - }, - metrics: defaults.metrics, - generatedAt: defaults.generatedAt, - }; - } - - listDatabaseBackups( - databaseId: string, - limit?: number, - ): { - backups: Array<{ - id: string; - backupType: string; - status: string; - size: number | null; - createdAt: string; - }>; - retentionDays: number | null; - hasMore: boolean; - } { - const backups = (this.data.databaseBackups ?? []).filter( - (backup) => backup.databaseId === databaseId, - ); - const limited = limit === undefined ? backups : backups.slice(0, limit); - - return { - backups: limited.map((backup) => ({ - id: backup.id, - backupType: backup.backupType, - status: backup.status, - size: backup.size ?? null, - createdAt: backup.createdAt, - })), - retentionDays: this.data.databaseBackupRetentionDays ?? null, - hasMore: limited.length < backups.length, - }; - } - - restoreDatabase(input: { - targetDatabaseId: string; - sourceDatabaseId: string; - backupId: string; - }): - | { outcome: "restored"; database: DatabaseRecord } - | { outcome: "target-not-found" } - | { outcome: "backup-not-found" } { - const target = this.getDatabase(input.targetDatabaseId); - if (!target) { - return { outcome: "target-not-found" }; - } - - const backup = (this.data.databaseBackups ?? []).find( - (candidate) => - candidate.id === input.backupId && - candidate.databaseId === input.sourceDatabaseId, - ); - if (!backup) { - return { outcome: "backup-not-found" }; - } - - target.status = "recovering"; - return { outcome: "restored", database: target }; - } - - rotateDatabaseConnection( - connectionId: string, - ): - | { connection: DatabaseConnectionRecord; connectionString: string } - | undefined { - const connection = this.getDatabaseConnection(connectionId); - if (!connection) { - return undefined; - } - - const connectionString = `postgresql://rotated-${connection.databaseId}-${connection.id}.example.prisma.io/postgres`; - connection.connectionString = connectionString; - return { connection, connectionString }; - } - - listBucketsForProject( - projectId: string, - branchName?: string, - ): BucketRecord[] { - return (this.data.buckets ?? []).filter( - (bucket) => - bucket.projectId === projectId && - (!branchName || - this.data.branches.find( - (branch) => - branch.id === bucket.branchId && branch.name === branchName, - )), - ); - } - - getBucket(bucketId: string): BucketRecord | undefined { - return (this.data.buckets ?? []).find((bucket) => bucket.id === bucketId); - } - - createBucket(input: { - projectId: string; - name?: string; - branchGitName?: string; - }): BucketRecord | undefined { - this.data.buckets ??= []; - - let branchId: string | null = null; - if (input.branchGitName) { - const branch = this.data.branches.find( - (b) => - b.projectId === input.projectId && b.name === input.branchGitName, - ); - if (!branch) { - return undefined; - } - branchId = branch.id; - } - - const bucket: BucketRecord = { - id: `bkt_${this.data.buckets.length + 1_000}`, - projectId: input.projectId, - branchId, - name: input.name ?? `bucket-${this.data.buckets.length + 1_000}`, - status: "ready", - createdAt: "2026-06-09T00:00:00.000Z", - }; - - this.data.buckets.push(bucket); - return bucket; - } - - deleteBucket(bucketId: string): BucketRecord | undefined { - this.data.buckets ??= []; - this.data.bucketKeys ??= []; - const bucket = this.getBucket(bucketId); - if (!bucket) { - return undefined; - } - - this.data.buckets = this.data.buckets.filter( - (candidate) => candidate.id !== bucketId, - ); - this.data.bucketKeys = this.data.bucketKeys.filter( - (key) => key.bucketId !== bucketId, - ); - return bucket; - } - - listBucketKeys(bucketId: string): BucketKeyRecord[] { - return (this.data.bucketKeys ?? []).filter( - (key) => key.bucketId === bucketId, - ); - } - - createBucketKey(input: { - bucketId: string; - name?: string; - role: "read" | "read_write"; - }): - | { - key: BucketKeyRecord; - secretAccessKey: string; - accessKeyId: string; - endpoint: string; - bucketName: string; - } - | undefined { - const bucket = this.getBucket(input.bucketId); - if (!bucket) { - return undefined; - } - - this.data.bucketKeys ??= []; - const secretAccessKey = `secret-${input.bucketId}-${this.data.bucketKeys.length + 1}`; - const accessKeyId = `AKIA${input.bucketId.toUpperCase().replace(/_/g, "")}${this.data.bucketKeys.length + 1}`; - const endpoint = `https://fly.storage.tigris.dev`; - const bucketName = bucket.name; - - const key: BucketKeyRecord = { - id: `bkey_${this.data.bucketKeys.length + 1_000}`, - bucketId: input.bucketId, - name: input.name ?? `key-${this.data.bucketKeys.length + 1_000}`, - role: input.role, - valueHint: `${accessKeyId.slice(0, 8)}...`, - createdAt: "2026-06-09T00:00:00.000Z", - secretAccessKey, - accessKeyId, - endpoint, - bucketName, - }; - - this.data.bucketKeys.push(key); - return { key, secretAccessKey, accessKeyId, endpoint, bucketName }; - } - - deleteBucketKey( - bucketId: string, - keyId: string, - ): BucketKeyRecord | undefined { - this.data.bucketKeys ??= []; - const key = (this.data.bucketKeys ?? []).find( - (candidate) => candidate.id === keyId && candidate.bucketId === bucketId, - ); - if (!key) { - return undefined; - } - - this.data.bucketKeys = this.data.bucketKeys.filter( - (candidate) => candidate.id !== keyId, - ); - return key; - } -} - -export type { - BranchRecord, - BucketKeyRecord, - BucketRecord, - DatabaseConnectionRecord, - DatabaseRecord, - DeploymentRecord, - ProjectRecord, - ProviderRecord, - UserRecord, - WorkspaceRecord, -}; diff --git a/packages/cli/src/auth/login.ts b/packages/cli/src/auth/login.ts index dcfdf29f..c19a87d2 100644 --- a/packages/cli/src/auth/login.ts +++ b/packages/cli/src/auth/login.ts @@ -125,9 +125,13 @@ export async function login(options: LoginOptions = {}): Promise { settle(resolve); } catch (error) { res.statusCode = 400; - const message = - error instanceof Error ? error.message : String(error); - res.end(message); + // The browser gets a fixed sentence. Whatever went wrong here + // is an internal failure, and its message can carry details of + // the exchange — or of the code that failed — to a page this + // process does not control. The operator still sees the real + // error: it is what this promise rejects with. + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.end("Sign-in could not be completed. Return to your terminal."); settle(() => reject(error)); return; } diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index f5673908..7ecfb5df 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -208,7 +208,6 @@ function resolveRuntime(options: RunCliOptions): CliRuntime { stdin: options.stdin ?? process.stdin, stdout: options.stdout ?? process.stdout, stderr: options.stderr ?? process.stderr, - fixturePath: options.fixturePath, stateDir: options.stateDir, }; } diff --git a/packages/cli/src/commands/auth/index.ts b/packages/cli/src/commands/auth/index.ts index e751b355..61205c49 100644 --- a/packages/cli/src/commands/auth/index.ts +++ b/packages/cli/src/commands/auth/index.ts @@ -1,7 +1,6 @@ import { Command, Option } from "commander"; import { - type AuthLoginCommandOptions, type AuthLogoutCommandOptions, runAuthLogin, runAuthLogout, @@ -55,11 +54,6 @@ function createAuthLoginCommand(runtime: CliRuntime): Command { "auth.login", ); - command - .addOption(new Option("--provider ").hideHelp()) - .addOption(new Option("--user ").hideHelp()) - .addOption(new Option("--workspace ").hideHelp()); - addGlobalFlags(command); command.action(async (options) => { @@ -67,7 +61,7 @@ function createAuthLoginCommand(runtime: CliRuntime): Command { runtime, "auth.login", options as Record, - (context) => runAuthLogin(context, options as AuthLoginCommandOptions), + (context) => runAuthLogin(context), { renderHuman: (context, descriptor, result) => renderAuthSuccess(context, descriptor, "auth.login", result), diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index 767edefd..1abbf593 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -186,13 +186,6 @@ const WAIT_TIMEOUT_UNIT_MULTIPLIER_MS: Record = { const NEXTJS_MENTION = /next\.?js/i; const STANDALONE_OUTPUT_MENTION = /standalone output/i; -function isRealMode(context: CommandContext): boolean { - return ( - !context.runtime.fixturePath && - !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH - ); -} - export async function runAppBuild( context: CommandContext, options?: { @@ -407,8 +400,6 @@ export async function runAppDeploy( appName: string | undefined, options?: AppDeployOptions, ): Promise> { - ensurePreviewAppMode(context); - const loaded = await loadComputeConfig( context.runtime.cwd, context.runtime.signal, @@ -995,8 +986,6 @@ export async function runAppListDeploys( projectRef?: string, configTarget?: string, ): Promise> { - ensurePreviewAppMode(context); - const compute = await resolveComputeManagementContext( context, configTarget, @@ -1083,8 +1072,6 @@ export async function runAppShow( projectRef?: string, configTarget?: string, ): Promise> { - ensurePreviewAppMode(context); - const compute = await resolveComputeManagementContext( context, configTarget, @@ -1179,8 +1166,6 @@ export async function runAppShowDeploy( context: CommandContext, deploymentId: string, ): Promise> { - ensurePreviewAppMode(context); - const provider = await requirePreviewAppProvider(context); const deployment = await provider .showDeployment(deploymentId, { signal: context.runtime.signal }) @@ -1245,8 +1230,6 @@ export async function runAppOpen( projectRef?: string, configTarget?: string, ): Promise> { - ensurePreviewAppMode(context); - const compute = await resolveComputeManagementContext( context, configTarget, @@ -1630,8 +1613,6 @@ export async function runAppLogs( projectRef?: string, configTarget?: string, ): Promise { - ensurePreviewAppMode(context); - const compute = await resolveComputeManagementContext( context, configTarget, @@ -1886,8 +1867,6 @@ export async function runAppPromote( projectRef?: string, configTarget?: string, ): Promise> { - ensurePreviewAppMode(context); - const compute = await resolveComputeManagementContext( context, configTarget, @@ -1988,8 +1967,6 @@ export async function runAppRollback( projectRef?: string, configTarget?: string, ): Promise> { - ensurePreviewAppMode(context); - const compute = await resolveComputeManagementContext( context, configTarget, @@ -2107,8 +2084,6 @@ export async function runAppRemove( configTarget?: string, branchName?: string, ): Promise> { - ensurePreviewAppMode(context); - const compute = await resolveComputeManagementContext( context, configTarget, @@ -2193,8 +2168,6 @@ async function resolveAppDomainTarget( }, commandName = "app domain", ): Promise { - ensurePreviewAppMode(context); - const compute = await resolveComputeManagementContext( context, options?.configTarget, @@ -4766,20 +4739,6 @@ function normalizeDeployRegionInput( }; } -function ensurePreviewAppMode(context: CommandContext) { - if (isRealMode(context)) { - return; - } - - throw featureUnavailableError( - "App commands are not available in fixture mode", - "Preview app commands require live app deployment integration.", - "Rerun without fixture mode enabled to use preview app deployment workflows.", - ["prisma-cli auth login", "prisma-cli project show"], - "app", - ); -} - function deployFailedError( summary: string, error: unknown, diff --git a/packages/cli/src/controllers/auth.ts b/packages/cli/src/controllers/auth.ts index 293414fe..d83922bf 100644 --- a/packages/cli/src/controllers/auth.ts +++ b/packages/cli/src/controllers/auth.ts @@ -29,9 +29,6 @@ import type { AuthWorkspaceLogoutResult, AuthWorkspaceUseResult, } from "../types/auth"; -import { createAuthUseCases } from "../use-cases/auth"; -import type { LoginSelection, SelectPromptPort } from "../use-cases/contracts"; -import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways"; import { createSelectPromptPort } from "./select-prompt-port"; export interface AuthLoginCommandOptions { @@ -50,34 +47,19 @@ function workspaceOperationContext( return { env: context.runtime.env, signal: context.runtime.signal }; } -function isRealMode(context: CommandContext): boolean { - return ( - !context.runtime.fixturePath && - !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH - ); -} - export async function runAuthLogin( context: CommandContext, - options: AuthLoginCommandOptions, ): Promise> { - let result: AuthStateResult; - - if (isRealMode(context)) { - const credential = await performLogin( - context.runtime.env, - context.runtime.signal, - ); - await storeLegacyCredential( - context.runtime.env, - credential, - context.runtime.signal, - ); - result = await readAuthState(context.runtime.env, context.runtime.signal); - } else { - const useCases = createAuthUseCases(createCliUseCaseGateways(context)); - result = await loginWithSelectionFlow(context, useCases, options); - } + const credential = await performLogin( + context.runtime.env, + context.runtime.signal, + ); + await storeLegacyCredential( + context.runtime.env, + credential, + context.runtime.signal, + ); + let result = await readAuthState(context.runtime.env, context.runtime.signal); const agentSetupTipCommand = await resolveAgentSetupTipCommand(context); if (agentSetupTipCommand) { @@ -99,15 +81,11 @@ export async function runAuthLogin( export async function runAuthLogout( context: CommandContext, ): Promise> { - let result: AuthStateResult; - - if (isRealMode(context)) { - await performLogout(context.runtime.env, context.runtime.signal); - result = await readAuthState(context.runtime.env, context.runtime.signal); - } else { - const useCases = createAuthUseCases(createCliUseCaseGateways(context)); - result = await useCases.logout(); - } + await performLogout(context.runtime.env, context.runtime.signal); + const result = await readAuthState( + context.runtime.env, + context.runtime.signal, + ); return createAuthSuccess("auth.logout", result, ["prisma-cli auth login"]); } @@ -115,14 +93,10 @@ export async function runAuthLogout( export async function runAuthWhoAmI( context: CommandContext, ): Promise> { - let result: AuthStateResult; - - if (isRealMode(context)) { - result = await readAuthState(context.runtime.env, context.runtime.signal); - } else { - const useCases = createAuthUseCases(createCliUseCaseGateways(context)); - result = await useCases.whoami(); - } + const result = await readAuthState( + context.runtime.env, + context.runtime.signal, + ); return createAuthSuccess( "auth.whoami", @@ -134,11 +108,7 @@ export async function runAuthWhoAmI( export async function runAuthWorkspaceList( context: CommandContext, ): Promise> { - const result = isRealMode(context) - ? await listAuthWorkspaces(workspaceOperationContext(context)) - : await createAuthUseCases( - createCliUseCaseGateways(context), - ).listWorkspaces(); + const result = await listAuthWorkspaces(workspaceOperationContext(context)); return { command: "auth.workspace.list", @@ -157,14 +127,10 @@ export async function runAuthWorkspaceUse( ? trimmedWorkspaceRef : await selectWorkspaceSession(context); - const result = isRealMode(context) - ? await switchAuthWorkspace( - workspaceOperationContext(context), - selectedWorkspaceRef, - ) - : await createAuthUseCases(createCliUseCaseGateways(context)).useWorkspace( - selectedWorkspaceRef, - ); + const result = await switchAuthWorkspace( + workspaceOperationContext(context), + selectedWorkspaceRef, + ); return { command: "auth.workspace.use", @@ -188,14 +154,10 @@ export async function runAuthWorkspaceLogout( ); } - const result = isRealMode(context) - ? await logoutAuthWorkspace( - workspaceOperationContext(context), - workspaceRef, - ) - : await createAuthUseCases( - createCliUseCaseGateways(context), - ).logoutWorkspace(workspaceRef); + const result = await logoutAuthWorkspace( + workspaceOperationContext(context), + workspaceRef, + ); return { command: "auth.workspace.logout", @@ -213,34 +175,10 @@ export async function runAuthWorkspaceLogout( export async function requireAuthenticatedAuthState( context: CommandContext, ): Promise { - if (isRealMode(context)) { - const current = await readAuthState( - context.runtime.env, - context.runtime.signal, - ); - if (current.authenticated) { - return current; - } - - if (!canPrompt(context)) { - throw authRequiredError(); - } - - const credential = await performLogin( - context.runtime.env, - context.runtime.signal, - ); - await storeLegacyCredential( - context.runtime.env, - credential, - context.runtime.signal, - ); - return readAuthState(context.runtime.env, context.runtime.signal); - } - - const useCases = createAuthUseCases(createCliUseCaseGateways(context)); - const current = await useCases.whoami(); - + const current = await readAuthState( + context.runtime.env, + context.runtime.signal, + ); if (current.authenticated) { return current; } @@ -249,22 +187,26 @@ export async function requireAuthenticatedAuthState( throw authRequiredError(); } - return loginWithSelectionFlow(context, useCases, {}); + const credential = await performLogin( + context.runtime.env, + context.runtime.signal, + ); + await storeLegacyCredential( + context.runtime.env, + credential, + context.runtime.signal, + ); + return readAuthState(context.runtime.env, context.runtime.signal); } async function selectWorkspaceSession( context: CommandContext, ): Promise { - const realMode = isRealMode(context); - if (realMode && context.runtime.env[SERVICE_TOKEN_ENV_VAR] !== undefined) { + if (context.runtime.env[SERVICE_TOKEN_ENV_VAR] !== undefined) { throw workspaceSwitchUnavailableError(); } - const result = realMode - ? await listAuthWorkspaces(workspaceOperationContext(context)) - : await createAuthUseCases( - createCliUseCaseGateways(context), - ).listWorkspaces(); + const result = await listAuthWorkspaces(workspaceOperationContext(context)); const workspaces = result.workspaces.filter( (workspace) => workspace.switchable, ); @@ -305,120 +247,6 @@ async function selectWorkspaceSession( return selected.id; } -async function loginWithSelectionFlow( - context: CommandContext, - useCases: ReturnType, - options: AuthLoginCommandOptions, -): Promise { - const prompt = canPrompt(context) ? createSelectPromptPort(context) : null; - const selection = await resolveLoginSelection(useCases, prompt, options); - return useCases.login(selection); -} - -async function resolveLoginSelection( - useCases: ReturnType, - prompt: SelectPromptPort | null, - options: AuthLoginCommandOptions, -): Promise { - const provider = options.provider - ? (await useCases.resolveProvider(options.provider)).id - : (await selectProvider(useCases, prompt)).id; - const user = options.user - ? await useCases.resolveUserForProvider(provider, options.user) - : await selectUser(useCases, prompt, provider); - const workspace = options.workspace - ? await useCases.resolveWorkspaceForUser(user.id, options.workspace) - : await selectWorkspace(useCases, prompt, user.id); - - return { - provider, - userId: user.id, - workspaceId: workspace.id, - }; -} - -async function selectProvider( - useCases: ReturnType, - prompt: SelectPromptPort | null, -) { - if (!prompt) { - throw nonInteractiveLoginError( - "Re-run prisma-cli auth login in a TTY, or provide --provider and --user, and --workspace when required.", - ); - } - - const providers = await useCases.listProviders(); - return prompt.select({ - message: "Select a provider", - choices: providers.map((provider) => ({ - label: provider.name, - value: provider, - })), - }); -} - -async function selectUser( - useCases: ReturnType, - prompt: SelectPromptPort | null, - provider: LoginSelection["provider"], -) { - const users = await useCases.listUsersForProvider(provider); - - if (!prompt) { - throw nonInteractiveLoginError( - "Re-run prisma-cli auth login in a TTY, or provide --provider and --user, and --workspace when required.", - ); - } - - return prompt.select({ - message: "Select a user", - choices: users.map((user) => ({ - label: `${user.name} <${user.email}>`, - value: user, - })), - }); -} - -async function selectWorkspace( - useCases: ReturnType, - prompt: SelectPromptPort | null, - userId: string, -) { - const workspaces = await useCases.listWorkspacesForUser(userId); - - if (workspaces.length === 1) { - return workspaces[0]; - } - - if (!prompt) { - throw usageError( - "Login requires explicit selectors in non-interactive mode", - "The selected mock user belongs to more than one workspace and the shell cannot prompt in the current mode.", - "Re-run prisma-cli auth login in a TTY, or provide --workspace.", - ["prisma-cli auth login"], - "auth", - ); - } - - return prompt.select({ - message: "Select a workspace", - choices: workspaces.map((workspace) => ({ - label: `${workspace.name} (${workspace.id})`, - value: workspace, - })), - }); -} - -function nonInteractiveLoginError(fix: string) { - return usageError( - "Login requires explicit selectors in non-interactive mode", - "The fixture mode cannot prompt in the current mode.", - fix, - ["prisma-cli auth login"], - "auth", - ); -} - function createAuthSuccess( command: "auth.login" | "auth.logout" | "auth.whoami", result: AuthStateResult, diff --git a/packages/cli/src/controllers/branch.ts b/packages/cli/src/controllers/branch.ts index 34c38105..6d30660c 100644 --- a/packages/cli/src/controllers/branch.ts +++ b/packages/cli/src/controllers/branch.ts @@ -17,18 +17,9 @@ import type { BranchRole, BranchSummary, } from "../types/branch"; -import { createBranchUseCases } from "../use-cases/branch"; -import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways"; import { requireAuthenticatedAuthState } from "./auth"; import { listRealWorkspaceProjects } from "./project"; -function isRealMode(context: CommandContext): boolean { - return ( - !context.runtime.fixturePath && - !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH - ); -} - export interface RawBranchRecord { id: string; gitName: string; @@ -38,21 +29,9 @@ export interface RawBranchRecord { export async function runBranchList( context: CommandContext, ): Promise> { - if (isRealMode(context)) { - return { - command: "branch.list", - result: await listRealBranches(context), - warnings: [], - nextSteps: [], - }; - } - - const useCases = createBranchUseCases(createCliUseCaseGateways(context)); - const result = await useCases.list(); - return { command: "branch.list", - result, + result: await listRealBranches(context), warnings: [], nextSteps: [], }; diff --git a/packages/cli/src/controllers/bucket.ts b/packages/cli/src/controllers/bucket.ts index d6857ea2..6819cb16 100644 --- a/packages/cli/src/controllers/bucket.ts +++ b/packages/cli/src/controllers/bucket.ts @@ -2,8 +2,6 @@ import { authenticatedManagementApiClient } from "../auth/guard"; import { type BucketProvider, createManagementBucketProvider, - normalizeBucket, - normalizeKey, } from "../lib/bucket/provider"; import { projectResolutionErrorToCliError, @@ -26,10 +24,7 @@ import type { BucketListResult, } from "../types/bucket"; import { requireAuthenticatedAuthState } from "./auth"; -import { - listFixtureWorkspaceProjects, - listRealWorkspaceProjects, -} from "./project"; +import { listRealWorkspaceProjects } from "./project"; interface BucketCommandFlags { projectRef?: string; @@ -54,13 +49,6 @@ interface ResolvedBucketContext { target: ResolvedProjectTarget; } -function isRealMode(context: CommandContext): boolean { - return ( - !context.runtime.fixturePath && - !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH - ); -} - export async function runBucketList( context: CommandContext, flags: BucketCommandFlags, @@ -271,17 +259,14 @@ export async function runBucketKeyDelete( async function resolveBucketProvider( context: CommandContext, ): Promise { - if (isRealMode(context)) { - const client = await authenticatedManagementApiClient( - context.runtime.env, - context.runtime.signal, - ); - if (!client) { - throw authRequiredError(); - } - return createManagementBucketProvider(client); + const client = await authenticatedManagementApiClient( + context.runtime.env, + context.runtime.signal, + ); + if (!client) { + throw authRequiredError(); } - return createFixtureBucketProvider(context); + return createManagementBucketProvider(client); } async function requireBucketContext( @@ -295,38 +280,20 @@ async function requireBucketContext( throw workspaceRequiredError(); } - if (isRealMode(context)) { - const client = await authenticatedManagementApiClient( - context.runtime.env, - context.runtime.signal, - ); - if (!client) { - throw authRequiredError(); - } - - const targetResult = await resolveProjectTarget({ - context, - workspace, - explicitProject: flags.projectRef, - listProjects: () => - listRealWorkspaceProjects(client, context.runtime.signal), - commandName, - }); - if (targetResult.isErr()) { - throw projectResolutionErrorToCliError(targetResult.error); - } - - return { - provider: createManagementBucketProvider(client), - target: targetResult.value, - }; + const client = await authenticatedManagementApiClient( + context.runtime.env, + context.runtime.signal, + ); + if (!client) { + throw authRequiredError(); } const targetResult = await resolveProjectTarget({ context, workspace, explicitProject: flags.projectRef, - listProjects: async () => listFixtureWorkspaceProjects(context, workspace), + listProjects: () => + listRealWorkspaceProjects(client, context.runtime.signal), commandName, }); if (targetResult.isErr()) { @@ -334,7 +301,7 @@ async function requireBucketContext( } return { - provider: createFixtureBucketProvider(context), + provider: createManagementBucketProvider(client), target: targetResult.value, }; } @@ -346,105 +313,6 @@ async function requireBucketProviderOnly( return resolveBucketProvider(context); } -function createFixtureBucketProvider(context: CommandContext): BucketProvider { - return { - async listBuckets(options) { - return context.api - .listBucketsForProject(options.projectId, options.branchName) - .map((bucket) => normalizeBucket(bucket)); - }, - - async createBucket(options) { - const created = context.api.createBucket({ - projectId: options.projectId, - name: options.name, - branchGitName: options.branchGitName, - }); - if (!created) { - throw branchNotFoundError(options.branchGitName ?? ""); - } - return normalizeBucket(created); - }, - - async deleteBucket(bucketId) { - const removed = context.api.deleteBucket(bucketId); - if (!removed) { - throw bucketNotFoundError(bucketId); - } - }, - - async listKeys(bucketId) { - if (!context.api.getBucket(bucketId)) { - throw bucketNotFoundError(bucketId); - } - return context.api - .listBucketKeys(bucketId) - .map((key) => normalizeKey(key)); - }, - - async createKey(options) { - const created = context.api.createBucketKey({ - bucketId: options.bucketId, - name: options.name, - role: options.role, - }); - if (!created) { - throw bucketNotFoundError(options.bucketId); - } - return { - key: normalizeKey(created.key), - secretAccessKey: created.secretAccessKey, - accessKeyId: created.accessKeyId, - endpoint: created.endpoint, - bucketName: created.bucketName, - }; - }, - - async deleteKey(bucketId, keyId) { - const removed = context.api.deleteBucketKey(bucketId, keyId); - if (!removed) { - throw keyNotFoundError(keyId, bucketId); - } - }, - }; -} - function resolveKeyRole(role: string | undefined): "read" | "read_write" { return role === "read" ? "read" : "read_write"; } - -function branchNotFoundError(branchGitName: string): CliError { - return new CliError({ - code: "BRANCH_NOT_FOUND", - domain: "bucket", - summary: "Branch not found", - why: `No branch matched "${branchGitName}" in the resolved project.`, - fix: "Pass a branch git name from prisma-cli branch list.", - exitCode: 1, - nextSteps: ["prisma-cli branch list"], - }); -} - -function bucketNotFoundError(bucketId: string): CliError { - return new CliError({ - code: "BUCKET_NOT_FOUND", - domain: "bucket", - summary: "Bucket not found", - why: `No bucket matched "${bucketId}".`, - fix: "Pass a bucket id from prisma-cli bucket list.", - exitCode: 1, - nextSteps: ["prisma-cli bucket list"], - }); -} - -function keyNotFoundError(keyId: string, bucketId: string): CliError { - return new CliError({ - code: "BUCKET_KEY_NOT_FOUND", - domain: "bucket", - summary: "Bucket key not found", - why: `No key matched "${keyId}" for bucket "${bucketId}".`, - fix: "Pass a key id from prisma-cli bucket key list .", - exitCode: 1, - nextSteps: [`prisma-cli bucket key list ${bucketId}`], - }); -} diff --git a/packages/cli/src/controllers/database.ts b/packages/cli/src/controllers/database.ts index b0697ed4..0a4e05ef 100644 --- a/packages/cli/src/controllers/database.ts +++ b/packages/cli/src/controllers/database.ts @@ -7,8 +7,6 @@ import { import { createManagementDatabaseProvider, type DatabaseProvider, - normalizeConnection, - normalizeDatabase, } from "../lib/database/provider"; import { projectResolutionErrorToCliError, @@ -38,10 +36,7 @@ import type { DatabaseUsageResult, } from "../types/database"; import { requireAuthenticatedAuthState } from "./auth"; -import { - listFixtureWorkspaceProjects, - listRealWorkspaceProjects, -} from "./project"; +import { listRealWorkspaceProjects } from "./project"; interface DatabaseCommandFlags { projectRef?: string; @@ -88,13 +83,6 @@ interface ResolvedDatabaseContext { target: ResolvedProjectTarget; } -function isRealMode(context: CommandContext): boolean { - return ( - !context.runtime.fixturePath && - !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH - ); -} - export async function runDatabaseList( context: CommandContext, flags: DatabaseCommandFlags, @@ -713,15 +701,15 @@ async function requireDatabaseContext( throw workspaceRequiredError(); } - if (isRealMode(context)) { - const client = await authenticatedManagementApiClient( - context.runtime.env, - context.runtime.signal, - ); - if (!client) { - throw authRequiredError(); - } + const client = await authenticatedManagementApiClient( + context.runtime.env, + context.runtime.signal, + ); + if (!client) { + throw authRequiredError(); + } + { const targetResult = await resolveProjectTarget({ context, workspace, @@ -744,22 +732,6 @@ async function requireDatabaseContext( target: targetResult.value, }; } - - const targetResult = await resolveProjectTarget({ - context, - workspace, - explicitProject: flags.projectRef, - listProjects: async () => listFixtureWorkspaceProjects(context, workspace), - commandName, - }); - if (targetResult.isErr()) { - throw projectResolutionErrorToCliError(targetResult.error); - } - - return { - provider: createFixtureDatabaseProvider(context), - target: targetResult.value, - }; } async function requireDatabaseProviderOnly( @@ -767,146 +739,19 @@ async function requireDatabaseProviderOnly( ): Promise { const authState = await requireAuthenticatedAuthState(context); - if (isRealMode(context)) { - const client = await authenticatedManagementApiClient( - context.runtime.env, - context.runtime.signal, - ); - if (!client) { - throw authRequiredError(); - } - return createManagementDatabaseProvider(client, { - formatCommand: resolvePrismaCliPackageCommandFormatterSync( - context.runtime.cwd, - ), - workspaceId: authState.workspace?.id, - }); + const client = await authenticatedManagementApiClient( + context.runtime.env, + context.runtime.signal, + ); + if (!client) { + throw authRequiredError(); } - - return createFixtureDatabaseProvider(context); -} - -function createFixtureDatabaseProvider( - context: CommandContext, -): DatabaseProvider { - return { - async listDatabases(options) { - return context.api - .listDatabasesForProject(options.projectId, options.branchName) - .map((database) => normalizeDatabase(database, database.projectId)); - }, - - async showDatabase(databaseId) { - const database = context.api.getDatabase(databaseId); - return database ? normalizeDatabase(database, database.projectId) : null; - }, - - async createDatabase(options) { - const created = context.api.createDatabase(options); - return { - database: normalizeDatabase( - created.database, - created.database.projectId, - ), - connection: normalizeConnection( - created.connection, - created.connection.databaseId, - ), - connectionString: created.connectionString, - }; - }, - - async removeDatabase(databaseId) { - const removed = context.api.removeDatabase(databaseId); - if (!removed) { - throw databaseNotFoundError(databaseId); - } - }, - - async listConnections(databaseId) { - if (!context.api.getDatabase(databaseId)) { - throw databaseNotFoundError(databaseId); - } - return context.api - .listDatabaseConnections(databaseId) - .map((connection) => - normalizeConnection(connection, connection.databaseId), - ); - }, - - async createConnection(options) { - const created = context.api.createDatabaseConnection(options); - if (!created) { - throw databaseNotFoundError(options.databaseId); - } - return { - connection: normalizeConnection( - created.connection, - created.connection.databaseId, - ), - connectionString: created.connectionString, - }; - }, - - async removeConnection(connectionId) { - const removed = context.api.removeDatabaseConnection(connectionId); - if (!removed) { - throw connectionNotFoundError(connectionId); - } - }, - - async getUsage(databaseId, options) { - if (!context.api.getDatabase(databaseId)) { - throw databaseNotFoundError(databaseId); - } - return context.api.getDatabaseUsage(databaseId, { - from: options?.from, - to: options?.to, - }); - }, - - async listBackups(databaseId, options) { - if (!context.api.getDatabase(databaseId)) { - throw databaseNotFoundError(databaseId); - } - return context.api.listDatabaseBackups(databaseId, options?.limit); - }, - - async restoreDatabase(options) { - const restored = context.api.restoreDatabase({ - targetDatabaseId: options.targetDatabaseId, - sourceDatabaseId: options.sourceDatabaseId, - backupId: options.backupId, - }); - if (restored.outcome === "target-not-found") { - throw databaseNotFoundError(options.targetDatabaseId); - } - if (restored.outcome === "backup-not-found") { - throw backupNotFoundError( - options.backupId, - options.sourceDatabaseId, - resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd), - ); - } - return normalizeDatabase(restored.database, options.projectId); - }, - - async rotateConnection(connectionId) { - const rotated = context.api.rotateDatabaseConnection(connectionId); - if (!rotated) { - throw connectionNotFoundError(connectionId); - } - const database = context.api.getDatabase(rotated.connection.databaseId); - return { - connection: normalizeConnection( - rotated.connection, - rotated.connection.databaseId, - ), - database: database ? { id: database.id, name: database.name } : null, - connectionString: rotated.connectionString, - }; - }, - }; + return createManagementDatabaseProvider(client, { + formatCommand: resolvePrismaCliPackageCommandFormatterSync( + context.runtime.cwd, + ), + workspaceId: authState.workspace?.id, + }); } export async function resolveDatabase( @@ -1058,37 +903,3 @@ function databaseAmbiguousError( }, }); } - -function backupNotFoundError( - backupId: string, - sourceDatabaseId: string, - formatCommand: PrismaCliPackageCommandFormatter, -): CliError { - const listCommand = formatCommand([ - "database", - "backup", - "list", - sourceDatabaseId, - ]); - return new CliError({ - code: "DATABASE_BACKUP_NOT_FOUND", - domain: "database", - summary: "Database backup not found", - why: `No backup matched "${backupId}" for database "${sourceDatabaseId}".`, - fix: `Pass a backup id from ${listCommand}.`, - exitCode: 1, - nextSteps: [listCommand], - }); -} - -function connectionNotFoundError(connectionId: string): CliError { - return new CliError({ - code: "DATABASE_CONNECTION_NOT_FOUND", - domain: "database", - summary: "Database connection not found", - why: `No database connection matched "${connectionId}".`, - fix: "Pass a connection id from prisma-cli database connection list .", - exitCode: 1, - nextSteps: ["prisma-cli database connection list "], - }); -} diff --git a/packages/cli/src/controllers/project.ts b/packages/cli/src/controllers/project.ts index 4423d041..3294f20f 100644 --- a/packages/cli/src/controllers/project.ts +++ b/packages/cli/src/controllers/project.ts @@ -37,9 +37,6 @@ import { createManagementProjectProvider, type ProjectProvider, projectApiError, - projectRemoveBlockedError, - projectRenameFailedError, - projectTransferRejectedError, } from "../lib/project/provider"; import { buildProjectSetupNextActions, @@ -64,7 +61,6 @@ import { formatCommandArgument } from "../shell/command-arguments"; import { authRequiredError, CliError, - featureUnavailableError, usageError, workspaceRequiredError, } from "../shell/errors"; @@ -83,8 +79,6 @@ import type { ProjectSummary, ProjectTransferResult, } from "../types/project"; -import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways"; -import { createProjectUseCases } from "../use-cases/project"; import { requireAuthenticatedAuthState } from "./auth"; export interface GitConnectOptions { @@ -98,13 +92,6 @@ export interface GitDisconnectOptions { export const GITHUB_INSTALL_POLL_INTERVAL_MS = 2_000; export 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 - ); -} - export async function readProjectListLocalBinding( cwd: string, projects: Array>, @@ -154,44 +141,19 @@ export async function runProjectList( throw workspaceRequiredError(); } - if (isRealMode(context)) { - const client = await authenticatedManagementApiClient( - context.runtime.env, - context.runtime.signal, - ); - if (!client) { - throw authRequiredError(); - } - const projects = sortProjects( - await listRealWorkspaceProjects(client, context.runtime.signal), - ); - const localBinding = await readProjectListLocalBinding( - context.runtime.cwd, - projects, - context.runtime.signal, - ); - const nextActions = buildProjectListNextActions(localBinding); - - return { - command: "project.list", - result: { - workspace, - projects: projects.map(toProjectSummary), - localBinding, - }, - warnings: [], - nextSteps: [], - nextActions, - }; + const client = await authenticatedManagementApiClient( + context.runtime.env, + context.runtime.signal, + ); + if (!client) { + throw authRequiredError(); } - - const projectUseCases = createProjectUseCases( - createCliUseCaseGateways(context), + const projects = sortProjects( + await listRealWorkspaceProjects(client, context.runtime.signal), ); - const result = await projectUseCases.list(authState); const localBinding = await readProjectListLocalBinding( context.runtime.cwd, - result.projects, + projects, context.runtime.signal, ); const nextActions = buildProjectListNextActions(localBinding); @@ -199,7 +161,8 @@ export async function runProjectList( return { command: "project.list", result: { - ...result, + workspace, + projects: projects.map(toProjectSummary), localBinding, }, warnings: [], @@ -232,13 +195,11 @@ export async function runProjectShow( throw workspaceRequiredError(); } - const result = isRealMode(context) - ? await resolveProjectShowInRealMode(context, workspace, explicitProject) - : await resolveProjectShowInFixtureMode( - context, - workspace, - explicitProject, - ); + const result = await resolveProjectShowInRealMode( + context, + workspace, + explicitProject, + ); return { command: "project.show", @@ -272,16 +233,6 @@ export async function runProjectCreate( throw projectSetupNameRequiredError("project create"); } - if (!isRealMode(context)) { - throw featureUnavailableError( - "Project create is not available in fixture mode", - "Creating Projects requires live platform integration.", - "Rerun without fixture mode enabled to create a Project.", - ["prisma-cli auth login"], - "project", - ); - } - const client = await authenticatedManagementApiClient( context.runtime.env, context.runtime.signal, @@ -345,21 +296,18 @@ export async function runProjectLink( throw workspaceRequiredError(); } - let provider: ReturnType | null = null; - let projects: ProjectCandidate[]; - if (isRealMode(context)) { - const client = await authenticatedManagementApiClient( - context.runtime.env, - context.runtime.signal, - ); - if (!client) { - throw authRequiredError(); - } - provider = createAppProvider(client); - projects = await listRealWorkspaceProjects(client, context.runtime.signal); - } else { - projects = listFixtureWorkspaceProjects(context, workspace); + const client = await authenticatedManagementApiClient( + context.runtime.env, + context.runtime.signal, + ); + if (!client) { + throw authRequiredError(); } + const provider = createAppProvider(client); + const projects = await listRealWorkspaceProjects( + client, + context.runtime.signal, + ); let result: ProjectSetupResult; if (projectRef?.trim()) { @@ -397,28 +345,18 @@ async function resolveInteractiveProjectLinkSetup( context: CommandContext, workspace: AuthWorkspace, projects: ProjectCandidate[], - provider: ReturnType | null, + provider: ReturnType, ): Promise { const setup = await promptForProjectSetupChoice({ context, projects, - createProject: (projectName) => { - if (!provider) { - throw featureUnavailableError( - "Project create is not available in fixture mode", - "Creating Projects requires live platform integration.", - "Rerun without fixture mode enabled to create a Project.", - ["prisma-cli auth login"], - "project", - ); - } - return createProjectForLinkSetup( + createProject: (projectName) => + 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.", @@ -747,10 +685,7 @@ async function resolveTransferRecipient( if (recipientToken) { return { accessToken: recipientToken, - workspaceId: isRealMode(context) - ? null - : // Fixture convention: the recipient token is the target workspace id. - recipientToken, + workspaceId: null, workspaceName: null, source: "recipient-token", }; @@ -761,10 +696,6 @@ async function resolveTransferRecipient( throw transferRecipientRequiredError(formatCommand); } - if (!isRealMode(context)) { - return resolveTransferRecipientInFixtureMode(context, workspaceRef); - } - if (context.runtime.env[SERVICE_TOKEN_ENV_VAR] !== undefined) { throw transferRecipientUnavailableError(formatCommand); } @@ -772,39 +703,6 @@ async function resolveTransferRecipient( return resolveTransferRecipientFromWorkspaceSession(context, workspaceRef); } -function resolveTransferRecipientInFixtureMode( - context: CommandContext, - workspaceRef: string, -): ResolvedTransferRecipient { - const workspaces = context.api.listWorkspaces(); - const matches = workspaces.filter( - (candidate) => - candidate.id === workspaceRef || - candidate.name.toLowerCase() === workspaceRef.toLowerCase(), - ); - const match = matches[0]; - if (match === undefined) { - throw workspaceNotAuthenticatedError(workspaceRef); - } - if (matches.length > 1) { - throw workspaceAmbiguousError( - workspaceRef, - matches.map((match) => ({ - id: match.id, - name: match.name, - credentialWorkspaceId: match.id, - })), - ); - } - return { - // Fixture transfers authorize by workspace id instead of a real token. - accessToken: match.id, - workspaceId: match.id, - workspaceName: match.name, - source: "workspace-session", - }; -} - async function resolveTransferRecipientFromWorkspaceSession( context: CommandContext, workspaceRef: string, @@ -856,19 +754,12 @@ interface ProjectMutationContext { async function requireProjectMutationContext( context: CommandContext, - workspace: AuthWorkspace, + _workspace: AuthWorkspace, ): Promise { - if (isRealMode(context)) { - const client = await requireProjectClient(context); - return { - provider: createManagementProjectProvider(client), - projects: await listRealWorkspaceProjects(client, context.runtime.signal), - }; - } - + const client = await requireProjectClient(context); return { - provider: createFixtureProjectProvider(context), - projects: listFixtureWorkspaceProjects(context, workspace), + provider: createManagementProjectProvider(client), + projects: await listRealWorkspaceProjects(client, context.runtime.signal), }; } @@ -878,12 +769,9 @@ async function requireProjectCommandContext( explicitProject: string | undefined, commandName: string, ): Promise<{ provider: ProjectProvider; target: ResolvedProjectTarget }> { - const realMode = isRealMode(context); - const client = realMode ? await requireProjectClient(context) : null; + const client = await requireProjectClient(context); const listProjects = async () => - client - ? listRealWorkspaceProjects(client, context.runtime.signal) - : listFixtureWorkspaceProjects(context, workspace); + listRealWorkspaceProjects(client, context.runtime.signal); const targetResult = await resolveProjectTarget({ context, @@ -896,11 +784,10 @@ async function requireProjectCommandContext( throw projectResolutionErrorToCliError(targetResult.error); } - const provider = client - ? createManagementProjectProvider(client) - : createFixtureProjectProvider(context); - - return { provider, target: targetResult.value }; + return { + provider: createManagementProjectProvider(client), + target: targetResult.value, + }; } async function requireProjectClient( @@ -916,58 +803,6 @@ async function requireProjectClient( return client; } -function createFixtureProjectProvider( - context: CommandContext, -): ProjectProvider { - const fixtureFormatCommand = resolvePrismaCliPackageCommandFormatterSync( - context.runtime.cwd, - ); - return { - async renameProject(options) { - const renamed = context.api.renameProject( - options.projectId, - options.name, - ); - if (!renamed) { - throw projectRenameFailedError(options.name, undefined); - } - return { - id: renamed.id, - name: renamed.name, - ...(renamed.url ? { url: renamed.url } : {}), - }; - }, - - async removeProject(options) { - const removed = context.api.removeProject(options.projectId); - if (removed.outcome === "blocked") { - throw projectRemoveBlockedError(options.projectId, undefined); - } - if (removed.outcome === "not-found") { - throw new CliError({ - code: "PROJECT_NOT_FOUND", - domain: "project", - summary: "Project not found", - why: `No project matched "${options.projectId}".`, - fix: `Pass a project id or name from ${fixtureFormatCommand(["project", "list"])}.`, - exitCode: 1, - nextSteps: [fixtureFormatCommand(["project", "list"])], - }); - } - }, - - async transferProject(options) { - const transferred = context.api.transferProject( - options.projectId, - options.recipientAccessToken, - ); - if (transferred.outcome !== "transferred") { - throw projectTransferRejectedError(options.projectId, undefined); - } - }, - }; -} - function requireProjectExactConfirmation(options: { id: string; confirm: string | undefined; @@ -1130,101 +965,30 @@ export async function runGitConnect( throw workspaceRequiredError(); } - if (isRealMode(context)) { - const client = await authenticatedManagementApiClient( - context.runtime.env, - context.runtime.signal, - ); - if (!client) { - throw authRequiredError(); - } - - 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, - ); - - if (existing) { - const existingConnection = toRepositoryConnection(existing); - if ( - repositoryFullNamesMatch( - existingConnection.repository.fullName, - repository.fullName, - ) - ) { - return { - command: "git.connect", - result: { - ...target, - repositoryConnection: existingConnection, - }, - warnings: [], - nextSteps: [], - }; - } - - 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, - }, - signal: context.runtime.signal, - }, - ); - - if (error || !data) { - throw repoConnectionApiError( - "Failed to connect GitHub repository", - response, - error, - ); - } - - return { - command: "git.connect", - result: { - ...target, - repositoryConnection: toRepositoryConnection(data.data), - }, - warnings: [], - nextSteps: [], - }; + const client = await authenticatedManagementApiClient( + context.runtime.env, + context.runtime.signal, + ); + if (!client) { + throw authRequiredError(); } - const target = await resolveRequiredProjectInFixtureMode( + const target = await resolveRequiredProjectInRealMode( context, workspace, options.project, "git connect", ); const repository = await resolveRepositoryForConnect(context, gitUrl); - const existingConnection = await context.stateStore.readRepositoryConnection( + const api = client; + const existing = await readFirstSourceRepository( + api, target.project.id, + context.runtime.signal, ); - if (existingConnection) { + if (existing) { + const existingConnection = toRepositoryConnection(existing); if ( repositoryFullNamesMatch( existingConnection.repository.fullName, @@ -1245,17 +1009,35 @@ export async function runGitConnect( throw repoAlreadyConnectedError(existingConnection.repository.fullName); } - const connection = createPendingRepositoryConnection(repository); - await context.stateStore.setRepositoryConnection( - target.project.id, - connection, + 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, + }); + + if (error || !data) { + throw repoConnectionApiError( + "Failed to connect GitHub repository", + response, + error, + ); + } return { command: "git.connect", result: { ...target, - repositoryConnection: connection, + repositoryConnection: toRepositoryConnection(data.data), }, warnings: [], nextSteps: [], @@ -1272,84 +1054,53 @@ export async function runGitDisconnect( throw workspaceRequiredError(); } - if (isRealMode(context)) { - const client = await authenticatedManagementApiClient( - context.runtime.env, - context.runtime.signal, - ); - if (!client) { - throw authRequiredError(); - } - - 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, - ); - - if (!existing) { - throw repoNotConnectedError(); - } - - const { error, response } = await api.DELETE( - "/v1/source-repositories/{id}", - { - params: { - path: { - id: existing.id, - }, - }, - signal: context.runtime.signal, - }, - ); - - if (error) { - throw repoConnectionApiError( - "Failed to disconnect GitHub repository", - response, - error, - ); - } - - return { - command: "git.disconnect", - result: { - ...target, - repositoryConnection: toRepositoryConnection(existing), - }, - warnings: [], - nextSteps: [], - }; + const client = await authenticatedManagementApiClient( + context.runtime.env, + context.runtime.signal, + ); + if (!client) { + throw authRequiredError(); } - const target = await resolveRequiredProjectInFixtureMode( + const target = await resolveRequiredProjectInRealMode( context, workspace, options.project, "git disconnect", ); - const existingConnection = await context.stateStore.readRepositoryConnection( + const api = client; + const existing = await readFirstSourceRepository( + api, target.project.id, + context.runtime.signal, ); - if (!existingConnection) { + if (!existing) { throw repoNotConnectedError(); } - await context.stateStore.clearRepositoryConnection(target.project.id); + const { error, response } = await api.DELETE("/v1/source-repositories/{id}", { + params: { + path: { + id: existing.id, + }, + }, + signal: context.runtime.signal, + }); + + if (error) { + throw repoConnectionApiError( + "Failed to disconnect GitHub repository", + response, + error, + ); + } return { command: "git.disconnect", result: { ...target, - repositoryConnection: existingConnection, + repositoryConnection: toRepositoryConnection(existing), }, warnings: [], nextSteps: [], @@ -1411,43 +1162,6 @@ async function resolveRequiredProjectInRealMode( return result.value; } -async function resolveProjectShowInFixtureMode( - context: CommandContext, - workspace: AuthWorkspace, - explicitProject: string | undefined, -): Promise { - const result = await inspectProjectBinding({ - context, - workspace, - explicitProject, - listProjects: async () => listFixtureWorkspaceProjects(context, workspace), - commandName: "project show", - }); - if (result.isErr()) { - throw projectResolutionErrorToCliError(result.error); - } - return result.value; -} - -async function resolveRequiredProjectInFixtureMode( - context: CommandContext, - workspace: AuthWorkspace, - explicitProject: string | undefined, - commandName: string, -): Promise { - const result = await resolveProjectTarget({ - context, - workspace, - explicitProject, - listProjects: async () => listFixtureWorkspaceProjects(context, workspace), - commandName, - }); - if (result.isErr()) { - throw projectResolutionErrorToCliError(result.error); - } - return result.value; -} - /** The projects the API returns for the active credential, sorted. * Takes no workspace: the credential names one, and the API answers * within it. */ @@ -1496,21 +1210,6 @@ export async function listRealWorkspaceProjects( ); } -export function listFixtureWorkspaceProjects( - context: CommandContext, - workspace: AuthWorkspace, -): ProjectCandidate[] { - return sortProjects( - context.api.listProjectsForWorkspace(workspace.id).map((project) => ({ - id: project.id, - name: project.name, - ...(project.url ? { url: project.url } : {}), - slug: project.slug, - workspace, - })), - ); -} - interface SourceRepositoryResponse { id: string; type?: "source-repository"; @@ -1566,122 +1265,6 @@ export interface SourceRepositoryApiError { }; } -interface SourceRepositoryApiResult { - data?: T; - error?: SourceRepositoryApiError; - response?: Response; -} - -export interface SourceRepositoryApiClient { - POST( - path: "/v1/source-repositories", - options: { - body: { - projectId: string; - provider: "github"; - providerRepositoryId: number; - installationId?: string; - }; - 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: { - params: { - query: { - projectId: string; - cursor?: string; - limit?: number; - }; - }; - signal?: AbortSignal; - }, - ): Promise< - SourceRepositoryApiResult<{ - data: SourceRepositoryResponse[]; - pagination: { - nextCursor: string | null; - hasMore: boolean; - }; - }> - >; - GET( - path: "/v1/scm-installations", - options: { - params: { - query: { - workspaceId: string; - cursor?: string; - limit?: number; - }; - }; - signal?: AbortSignal; - }, - ): Promise< - SourceRepositoryApiResult<{ - data: ScmInstallationResponse[]; - pagination: { - nextCursor: string | null; - hasMore: boolean; - }; - }> - >; - GET( - path: "/v1/scm-installations/{installationId}/repositories", - options: { - params: { - path: { - installationId: string; - }; - query: { - cursor?: string; - limit?: number; - }; - }; - signal?: AbortSignal; - }, - ): Promise< - SourceRepositoryApiResult<{ - data: ScmRepositoryResponse[]; - pagination: { - nextCursor: string | null; - hasMore: boolean; - }; - }> - >; - DELETE( - path: "/v1/source-repositories/{id}", - options: { - params: { - path: { - id: string; - }; - }; - signal?: AbortSignal; - }, - ): Promise>; -} - async function resolveRepositoryForConnect( context: CommandContext, gitUrl: string | undefined, @@ -1710,7 +1293,7 @@ async function resolveRepositoryForConnect( async function resolveInstalledRepository( context: CommandContext, - api: SourceRepositoryApiClient, + api: ManagementApiClient, workspaceId: string, repository: GitHubRepositoryReference, ): Promise { @@ -1765,7 +1348,7 @@ async function resolveInstalledRepository( } export async function findRepositoryInInstallations( - api: SourceRepositoryApiClient, + api: ManagementApiClient, installations: ScmInstallationResponse[], repository: GitHubRepositoryReference, signal: AbortSignal, @@ -1808,7 +1391,7 @@ export async function findRepositoryInInstallations( async function waitForInstalledRepository( context: CommandContext, - api: SourceRepositoryApiClient, + api: ManagementApiClient, workspaceId: string, repository: GitHubRepositoryReference, ): Promise<{ @@ -1911,7 +1494,7 @@ function sleep(ms: number, signal: AbortSignal): Promise { } export async function listScmInstallations( - api: SourceRepositoryApiClient, + api: ManagementApiClient, workspaceId: string, signal: AbortSignal, ): Promise { @@ -1953,7 +1536,7 @@ export async function listScmInstallations( } async function findRepositoryInInstallation( - api: SourceRepositoryApiClient, + api: ManagementApiClient, installationId: string, repository: GitHubRepositoryReference, signal: AbortSignal, @@ -2033,7 +1616,7 @@ function readNextPaginationCursor( } async function findRepositoryInInstallationIfAvailable( - api: SourceRepositoryApiClient, + api: ManagementApiClient, installationId: string, repository: GitHubRepositoryReference, signal: AbortSignal, @@ -2064,7 +1647,7 @@ function isUnavailableScmInstallationError(error: unknown): boolean { } export async function createGitHubInstallIntent( - api: SourceRepositoryApiClient, + api: ManagementApiClient, workspaceId: string, signal: AbortSignal, ): Promise { @@ -2111,7 +1694,7 @@ async function openInstallUrlIfInteractive( } export async function readFirstSourceRepository( - api: SourceRepositoryApiClient, + api: ManagementApiClient, projectId: string, signal: AbortSignal, ): Promise { @@ -2136,31 +1719,6 @@ export async function readFirstSourceRepository( return data.data[0] ?? null; } -function createPendingRepositoryConnection( - repository: GitHubRepositoryReference, -): GitRepositoryConnection { - return { - id: null, - provider: "github", - repoId: null, - repository, - defaultBranch: null, - isPrivate: null, - status: "pending", - installation: { - id: null, - status: "pending", - }, - automation: { - branches: false, - pullRequests: false, - comments: false, - }, - connectedAt: new Date().toISOString(), - updatedAt: null, - }; -} - export function toRepositoryConnection( record: SourceRepositoryResponse, ): GitRepositoryConnection { diff --git a/packages/cli/src/controllers/select-prompt-port.ts b/packages/cli/src/controllers/select-prompt-port.ts index d8b3e1a8..c5b56d2b 100644 --- a/packages/cli/src/controllers/select-prompt-port.ts +++ b/packages/cli/src/controllers/select-prompt-port.ts @@ -1,6 +1,21 @@ import { selectPrompt } from "../shell/prompt"; import type { CommandContext } from "../shell/runtime"; -import type { SelectPromptPort } from "../use-cases/contracts"; + +export interface SelectChoice { + label: string; + value: T; +} + +/** The narrow prompting capability a controller needs, so that the + * controllers which prompt do not each reach into the shell's prompt + * implementation. It lived in the use-case contracts until fixture mode + * was retired, and is kept here because two controllers still prompt. */ +export interface SelectPromptPort { + select(options: { + message: string; + choices: SelectChoice[]; + }): Promise; +} export function createSelectPromptPort( context: CommandContext, diff --git a/packages/cli/src/shell/runtime.ts b/packages/cli/src/shell/runtime.ts index be60a430..f99f7d23 100644 --- a/packages/cli/src/shell/runtime.ts +++ b/packages/cli/src/shell/runtime.ts @@ -1,6 +1,5 @@ import type { Command } from "commander"; import { LocalStateStore } from "../adapters/local-state"; -import { MockApi } from "../adapters/mock-api"; import { DEFAULT_STATE_DIR_NAME, resolveStateDir } from "../state-dir"; import type { GlobalFlags } from "./global-flags"; import { renderHelp } from "./help"; @@ -19,12 +18,10 @@ export interface CliRuntime { stdout: NodeJS.WriteStream; stderr: NodeJS.WriteStream; env: NodeJS.ProcessEnv; - fixturePath?: string; stateDir?: string; } export interface CommandContext { - api: MockApi; stateStore: LocalStateStore; output: CliOutput; flags: GlobalFlags; @@ -59,26 +56,9 @@ export async function createCommandContext( runtime: CliRuntime, flags: GlobalFlags, ): Promise { - const fixturePath = - runtime.fixturePath ?? runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH; const stateDir = await resolveStateDir(runtime); - // Load the mock API only when fixture mode is explicitly enabled. - let loadedApi: MockApi | undefined; - if (fixturePath) { - loadedApi = await MockApi.load(fixturePath, runtime.signal); - } - return { - get api(): MockApi { - if (!loadedApi) { - throw new Error( - "context.api accessed in real mode. Set runtime.fixturePath or PRISMA_CLI_MOCK_FIXTURE_PATH to use fixture mode.", - ); - } - - return loadedApi; - }, stateStore: new LocalStateStore(stateDir, runtime.signal), output: { stdout: runtime.stdout, diff --git a/packages/cli/src/use-cases/auth.ts b/packages/cli/src/use-cases/auth.ts deleted file mode 100644 index c6a05ce8..00000000 --- a/packages/cli/src/use-cases/auth.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { - workspaceAmbiguousError, - workspaceNotAuthenticatedError, -} from "../auth/errors"; -import { authRequiredError, usageError } from "../shell/errors"; -import type { - AuthProviderId, - AuthStateResult, - AuthWorkspace, -} from "../types/auth"; -import type { - AuthUseCases, - IdentityGateway, - LoginSelection, - SessionGateway, -} from "./contracts"; - -interface AuthUseCaseDependencies { - identityGateway: IdentityGateway; - sessionGateway: SessionGateway; -} - -export function createAuthUseCases( - dependencies: AuthUseCaseDependencies, -): AuthUseCases { - return { - whoami: () => resolveCurrentAuthState(dependencies), - login: async (selection: LoginSelection) => { - await dependencies.sessionGateway.writeAuthSession({ - provider: selection.provider, - userId: selection.userId, - workspaceId: selection.workspaceId, - }); - - return resolveCurrentAuthState(dependencies); - }, - logout: async () => { - await dependencies.sessionGateway.clearAuthSession(); - return resolveCurrentAuthState(dependencies); - }, - listWorkspaces: async () => { - const session = await dependencies.sessionGateway.readAuthSession(); - if (!session) { - return { - authSource: "none", - activeWorkspace: null, - workspaces: [], - }; - } - - const workspaces = dependencies.identityGateway.listUserWorkspaces( - session.userId, - ); - const activeWorkspace = - workspaces.find((workspace) => workspace.id === session.workspaceId) ?? - null; - - return { - authSource: "oauth", - activeWorkspace, - workspaces: workspaces.map((workspace) => ({ - ...workspace, - credentialWorkspaceId: workspace.id, - active: workspace.id === session.workspaceId, - source: "oauth" as const, - switchable: true, - lastSeenAt: null, - })), - }; - }, - useWorkspace: async (workspaceRef: string) => { - const session = await dependencies.sessionGateway.readAuthSession(); - if (!session) { - throw authRequiredError(["prisma-cli auth login"]); - } - - const ref = workspaceRef.trim(); - const workspaces = dependencies.identityGateway.listUserWorkspaces( - session.userId, - ); - const matches = workspaces.filter((workspace) => - workspaceMatchesRef(workspace, ref), - ); - - if (matches.length === 0) { - throw workspaceNotAuthenticatedError(workspaceRef); - } - - if (matches.length > 1) { - throw workspaceAmbiguousError( - workspaceRef, - matches.map((workspace) => ({ - id: workspace.id, - name: workspace.name, - credentialWorkspaceId: workspace.id, - })), - ); - } - - const selected = matches[0]; - const previousWorkspace = - dependencies.identityGateway.getWorkspace(session.workspaceId) ?? null; - await dependencies.sessionGateway.writeAuthSession({ - ...session, - workspaceId: selected.id, - }); - - return { - previousWorkspace, - workspace: selected, - }; - }, - logoutWorkspace: async (workspaceRef: string) => { - const session = await dependencies.sessionGateway.readAuthSession(); - if (!session) { - throw workspaceNotAuthenticatedError(workspaceRef); - } - - const ref = workspaceRef.trim(); - const workspaces = dependencies.identityGateway.listUserWorkspaces( - session.userId, - ); - const matches = workspaces.filter((workspace) => - workspaceMatchesRef(workspace, ref), - ); - - if (matches.length === 0) { - throw workspaceNotAuthenticatedError(workspaceRef); - } - - if (matches.length > 1) { - throw workspaceAmbiguousError( - workspaceRef, - matches.map((workspace) => ({ - id: workspace.id, - name: workspace.name, - credentialWorkspaceId: workspace.id, - })), - ); - } - - const workspace = matches[0]; - const wasActive = workspace.id === session.workspaceId; - const activeWorkspace = wasActive - ? null - : (dependencies.identityGateway.getWorkspace(session.workspaceId) ?? - null); - - if (wasActive) { - await dependencies.sessionGateway.clearAuthSession(); - } - - return { - workspace, - wasActive, - activeWorkspace, - }; - }, - listProviders: async () => dependencies.identityGateway.listProviders(), - resolveProvider: async (providerId) => { - const provider = dependencies.identityGateway.getProvider(providerId); - - if (!provider) { - throw usageError( - "Login requires a valid mock provider", - `The mock provider "${providerId}" does not exist.`, - "Use --provider github or --provider google.", - ["prisma-cli auth login"], - "auth", - ); - } - - return provider; - }, - listUsersForProvider: async (providerId: AuthProviderId) => { - const users = - dependencies.identityGateway.listUsersForProvider(providerId); - - if (users.length === 0) { - throw usageError( - "Login requires a valid mock user", - `No mock users support provider "${providerId}".`, - "Update the fixture data or choose a different provider.", - ["prisma-cli auth login"], - "auth", - ); - } - - return users; - }, - resolveUserForProvider: async ( - providerId: AuthProviderId, - userId: string, - ) => { - const user = dependencies.identityGateway.getUserForProvider( - providerId, - userId, - ); - - if (!user) { - throw usageError( - "Login requires a valid mock user", - `The mock user "${userId}" is not available for provider "${providerId}".`, - "Choose a user that supports the selected provider.", - ["prisma-cli auth login"], - "auth", - ); - } - - return user; - }, - listWorkspacesForUser: async (userId: string) => - dependencies.identityGateway.listUserWorkspaces(userId), - resolveWorkspaceForUser: async (userId: string, workspaceId: string) => { - const workspace = dependencies.identityGateway.getUserWorkspace( - userId, - workspaceId, - ); - - if (!workspace) { - throw usageError( - "Login requires a valid mock workspace", - `The mock workspace "${workspaceId}" is not available for the selected user.`, - "Choose a workspace that the selected user can access.", - ["prisma-cli auth login"], - "auth", - ); - } - - return workspace; - }, - }; -} - -function workspaceMatchesRef(workspace: AuthWorkspace, ref: string): boolean { - return ( - workspace.id === ref || workspace.name.toLowerCase() === ref.toLowerCase() - ); -} - -async function resolveCurrentAuthState( - dependencies: AuthUseCaseDependencies, -): Promise { - const session = await dependencies.sessionGateway.readAuthSession(); - - if (!session) { - return { - authenticated: false, - provider: null, - user: null, - workspace: null, - credential: null, - }; - } - - const provider = dependencies.identityGateway.getProvider(session.provider); - const user = dependencies.identityGateway.getUser(session.userId); - const workspace = dependencies.identityGateway.getWorkspace( - session.workspaceId, - ); - - if (!provider || !user || !workspace) { - return { - authenticated: false, - provider: null, - user: null, - workspace: null, - credential: null, - }; - } - - return { - authenticated: true, - provider: provider.id, - user: { - id: user.id, - email: user.email, - name: user.name, - }, - workspace, - credential: { - type: "oauth", - id: null, - name: null, - }, - }; -} diff --git a/packages/cli/src/use-cases/branch.ts b/packages/cli/src/use-cases/branch.ts deleted file mode 100644 index 0f4d76d0..00000000 --- a/packages/cli/src/use-cases/branch.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { BranchListResult, BranchSummary } from "../types/branch"; -import type { - BranchGateway, - BranchUseCases, - ProjectGateway, - ProjectStateGateway, - RemoteBranchRecord, -} from "./contracts"; - -interface BranchUseCaseDependencies { - branchGateway: BranchGateway; - projectGateway: ProjectGateway; - projectStateGateway: ProjectStateGateway; -} - -export function createBranchUseCases( - dependencies: BranchUseCaseDependencies, -): BranchUseCases { - return { - list: async (): Promise => { - const projectId = - await dependencies.projectStateGateway.readRememberedProjectId(); - if (!projectId) { - return { - projectId: "", - projectName: "not resolved", - branches: [], - }; - } - - const remoteBranches = await listRemoteBranches( - dependencies.branchGateway, - projectId, - ); - const projectName = resolveProjectName( - dependencies.projectGateway, - projectId, - ); - - return { - projectId, - projectName: projectName ?? "not resolved", - branches: buildBranchSummaries(remoteBranches), - }; - }, - }; -} - -function resolveProjectName( - projectGateway: ProjectGateway, - projectId: string | null, -): string | null { - if (!projectId) { - return null; - } - - return projectGateway.getProject(projectId)?.name ?? null; -} - -async function listRemoteBranches( - branchGateway: BranchGateway, - projectId: string | null, -): Promise { - if (!projectId) { - return []; - } - - 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 sortBranches(branches: BranchSummary[]): BranchSummary[] { - return branches.slice().sort((left, right) => { - const leftRank = branchOrder(left); - const rightRank = branchOrder(right); - - if (leftRank !== rightRank) { - return leftRank - rightRank; - } - - return left.name.localeCompare(right.name); - }); -} - -function branchOrder(branch: BranchSummary): number { - if (branch.role === "production") { - return 0; - } - - return 1; -} diff --git a/packages/cli/src/use-cases/contracts.ts b/packages/cli/src/use-cases/contracts.ts deleted file mode 100644 index 62f8c01a..00000000 --- a/packages/cli/src/use-cases/contracts.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { - AuthProviderId, - AuthStateResult, - AuthUser, - AuthWorkspace, - AuthWorkspaceListResult, - AuthWorkspaceLogoutResult, - AuthWorkspaceUseResult, -} from "../types/auth"; -import type { BranchListResult, BranchRole } from "../types/branch"; -import type { ProjectSummary } from "../types/project"; - -export interface ProviderInfo { - id: AuthProviderId; - name: string; -} - -export interface IdentityUser extends AuthUser { - id: string; - name: string; -} - -export interface ProjectRecord extends ProjectSummary { - workspaceId: string; -} - -export interface RemoteBranchRecord { - id: string; - projectId: string; - name: string; - role: BranchRole; - currentDeploymentId: string | null; -} - -export interface DeploymentRecord { - id: string; - status: string; - url: string | null; - projectId: string; - branch: string; -} - -export interface AuthSessionRecord { - provider: AuthProviderId; - userId: string; - workspaceId: string; -} - -export interface IdentityGateway { - listProviders(): ProviderInfo[]; - getProvider(providerId: string): ProviderInfo | undefined; - listUsersForProvider(providerId: AuthProviderId): IdentityUser[]; - getUser(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; -} - -export interface ProjectGateway { - listProjectsForWorkspace(workspaceId: string): ProjectRecord[]; - getProject(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; - getDeployment(deploymentId: string): DeploymentRecord | undefined; -} - -export interface SessionGateway { - readAuthSession(): Promise; - writeAuthSession(session: AuthSessionRecord): Promise; - clearAuthSession(): Promise; -} - -export interface ProjectStateGateway { - readRememberedProjectId(): Promise; -} - -export interface LoginSelection { - provider: AuthProviderId; - userId: string; - workspaceId: string; -} - -export interface SelectChoice { - label: string; - value: T; -} - -export interface SelectPromptPort { - select(options: { - message: string; - choices: SelectChoice[]; - }): Promise; -} - -export interface AuthUseCases { - whoami(): Promise; - login(selection: LoginSelection): Promise; - logout(): Promise; - listWorkspaces(): Promise; - useWorkspace(workspaceRef: string): Promise; - logoutWorkspace(workspaceRef: string): Promise; - listProviders(): Promise; - resolveProvider(providerId: string): Promise; - listUsersForProvider(providerId: AuthProviderId): Promise; - resolveUserForProvider( - providerId: AuthProviderId, - userId: string, - ): Promise; - listWorkspacesForUser(userId: string): Promise; - resolveWorkspaceForUser( - userId: string, - workspaceId: string, - ): Promise; -} - -export interface ProjectUseCases { - list( - authState: AuthStateResult, - ): Promise; - listProjectsForWorkspace(workspaceId: string): Promise; -} - -export interface BranchUseCases { - list(): Promise; -} diff --git a/packages/cli/src/use-cases/create-cli-gateways.ts b/packages/cli/src/use-cases/create-cli-gateways.ts deleted file mode 100644 index c5d76f26..00000000 --- a/packages/cli/src/use-cases/create-cli-gateways.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { CommandContext } from "../shell/runtime"; -import type { - BranchGateway, - IdentityGateway, - ProjectGateway, - ProjectStateGateway, - SessionGateway, -} from "./contracts"; - -export interface CliUseCaseGateways { - identityGateway: IdentityGateway; - projectGateway: ProjectGateway; - branchGateway: BranchGateway; - projectStateGateway: ProjectStateGateway; - sessionGateway: SessionGateway; -} - -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), - getUser: (userId) => { - const user = context.api.getUser(userId); - return user ? toAuthUser(user) : undefined; - }, - getUserForProvider: (providerId, userId) => { - const user = context.api.getUserForProvider(providerId, userId); - return user ? toAuthUser(user) : undefined; - }, - listUserWorkspaces: (userId) => - context.api.listUserWorkspaces(userId).map(toAuthWorkspace), - getWorkspace: (workspaceId) => { - const workspace = context.api.getWorkspace(workspaceId); - return workspace ? toAuthWorkspace(workspace) : undefined; - }, - getUserWorkspace: (userId, workspaceId) => { - const workspace = context.api.getUserWorkspace(userId, workspaceId); - return workspace ? toAuthWorkspace(workspace) : undefined; - }, - }, - projectGateway: { - listProjectsForWorkspace: (workspaceId) => - context.api.listProjectsForWorkspace(workspaceId), - getProject: (projectId) => context.api.getProject(projectId), - getProjectForWorkspace: (workspaceId, projectId) => - context.api.getProjectForWorkspace(workspaceId, projectId), - }, - branchGateway: { - listBranchesForProject: (projectId) => - context.api.listBranchesForProject(projectId), - getBranchForProject: (projectId, name) => { - return context.api.getBranchForProject(projectId, name); - }, - getDeployment: (deploymentId) => context.api.getDeployment(deploymentId), - }, - projectStateGateway: { - readRememberedProjectId: async () => { - const remembered = await context.stateStore.readLastResolvedProject(); - return remembered?.id ?? null; - }, - }, - sessionGateway: { - readAuthSession: async () => { - const state = await context.stateStore.read(); - return state.auth; - }, - writeAuthSession: async (session) => { - await context.stateStore.setAuthSession(session); - }, - clearAuthSession: async () => { - await context.stateStore.clearAuthSession(); - }, - }, - }; -} - -function toAuthUser(user: { id: string; name: string; email: string }) { - return { - id: user.id, - name: user.name, - email: user.email, - }; -} - -function toAuthWorkspace(workspace: { id: string; name: string }) { - return { - id: workspace.id, - name: workspace.name, - }; -} diff --git a/packages/cli/src/use-cases/project.ts b/packages/cli/src/use-cases/project.ts deleted file mode 100644 index d9143024..00000000 --- a/packages/cli/src/use-cases/project.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { authRequiredError, CliError } from "../shell/errors"; -import type { AuthStateResult } from "../types/auth"; -import type { ProjectListResult, ProjectSummary } from "../types/project"; -import type { ProjectGateway, ProjectUseCases } from "./contracts"; - -interface ProjectUseCaseDependencies { - projectGateway: ProjectGateway; -} - -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), - }; - }, - listProjectsForWorkspace: async ( - workspaceId: string, - ): Promise => - listSortedWorkspaceProjects(dependencies.projectGateway, workspaceId).map( - toProjectSummary, - ), - }; -} - -function requireWorkspace(authState: AuthStateResult) { - if (!authState.authenticated || !authState.workspace) { - throw authRequiredError(); - } - - return authState.workspace; -} - -function listSortedWorkspaceProjects( - projectGateway: ProjectGateway, - workspaceId: string, -) { - return projectGateway - .listProjectsForWorkspace(workspaceId) - .slice() - .sort( - (left, right) => - left.name.localeCompare(right.name) || left.id.localeCompare(right.id), - ); -} - -function toProjectSummary(project: { - id: string; - name: string; - url?: string; - defaultRegion?: string | null; -}): ProjectSummary { - return { - id: project.id, - name: project.name, - ...(project.url ? { url: project.url } : {}), - ...(project.defaultRegion != null - ? { defaultRegion: project.defaultRegion } - : {}), - }; -} - -export function projectNotFoundError( - why: string, - fix: string, - nextSteps: string[] = ["prisma-cli project list"], -): CliError { - return new CliError({ - code: "PROJECT_NOT_FOUND", - domain: "project", - summary: "Project not found", - why, - fix, - exitCode: 1, - nextSteps, - }); -} diff --git a/packages/cli/src/v8/git/connect.ts b/packages/cli/src/v8/git/connect.ts index 996b87a8..dad56323 100644 --- a/packages/cli/src/v8/git/connect.ts +++ b/packages/cli/src/v8/git/connect.ts @@ -1,4 +1,5 @@ /** The `git connect` command. */ + import { type Block, defineCommand, @@ -6,6 +7,7 @@ import { positional, } from "@prisma/cli-engine"; import { CliStructuredError, notOk, ok } from "@prisma/cli-engine/protocol"; +import type { ManagementApiClient } from "@prisma/management-api-sdk"; import type { GitHubRepositoryReference } from "../../adapters/git"; import { parseGitHubRepositoryUrl, @@ -24,7 +26,6 @@ import { repoAlreadyConnectedError, repoConnectionApiError, repositoryFullNamesMatch, - type SourceRepositoryApiClient, toRepositoryConnection, unsupportedRepositoryProviderError, } from "../../controllers/project"; @@ -51,7 +52,7 @@ const WAIT_MESSAGE = */ async function resolveInstalledRepository( ctx: GitCommandContext, - api: SourceRepositoryApiClient, + api: ManagementApiClient, workspaceId: string, repository: GitHubRepositoryReference, ): Promise { diff --git a/packages/cli/src/v8/git/context.ts b/packages/cli/src/v8/git/context.ts index 341435be..403ceb86 100644 --- a/packages/cli/src/v8/git/context.ts +++ b/packages/cli/src/v8/git/context.ts @@ -1,7 +1,7 @@ /** Workspace, project and the source-repository client for the * `git *` commands. */ import { type CommandContext, flag } from "@prisma/cli-engine"; -import type { SourceRepositoryApiClient } from "../../controllers/project"; +import type { ManagementApiClient } from "@prisma/management-api-sdk"; import type { ResolvedProjectTarget } from "../../lib/project/resolution"; import { resolvePinnedProject } from "../project/context"; import { resolveActiveWorkspace } from "../resources-shared/workspace"; @@ -14,7 +14,7 @@ export const projectFlag = flag.string({ }); export interface GitContext { - readonly api: SourceRepositoryApiClient; + readonly api: ManagementApiClient; readonly target: ResolvedProjectTarget; } @@ -35,5 +35,5 @@ export async function resolveGitContext( // ctx.api: the same methods, typed to the handful of paths they call. // Structurally compatible, but neither type is declared in terms of // the other, so the compiler needs the cast spelled out. - return { api: ctx.api as unknown as SourceRepositoryApiClient, target }; + return { api: ctx.api, target }; } diff --git a/packages/cli/tests/app-branch-database.test.ts b/packages/cli/tests/app-branch-database.test.ts index 926b03fd..4dce377b 100644 --- a/packages/cli/tests/app-branch-database.test.ts +++ b/packages/cli/tests/app-branch-database.test.ts @@ -167,7 +167,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -330,7 +329,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -479,7 +477,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -595,7 +592,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -725,7 +721,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -856,7 +851,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -995,7 +989,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1137,7 +1130,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1269,7 +1261,6 @@ describe("app deploy branch database setup", () => { isTTY: true, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1365,7 +1356,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1447,7 +1437,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1518,7 +1507,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1619,7 +1607,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1845,7 +1832,6 @@ describe("app deploy branch database setup", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); diff --git a/packages/cli/tests/app-controller.test.ts b/packages/cli/tests/app-controller.test.ts index e43ae105..54351258 100644 --- a/packages/cli/tests/app-controller.test.ts +++ b/packages/cli/tests/app-controller.test.ts @@ -254,7 +254,6 @@ async function setupAgentPromptDeployTest(options: { isTTY: true, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -333,7 +332,6 @@ describe("app controller", () => { stateDir: path.join(cwd, ".state"), env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -430,7 +428,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -501,7 +498,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -534,7 +530,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -591,7 +586,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -650,7 +644,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_APP_ID: "app_frontend", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -715,7 +708,6 @@ describe("app controller", () => { stateDir: path.join(cwd, ".state"), env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -758,7 +750,6 @@ describe("app controller", () => { stateDir: path.join(cwd, ".state"), env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -856,7 +847,6 @@ describe("app controller", () => { stateDir: path.join(repoDir, ".state"), env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -928,7 +918,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1105,7 +1094,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1181,7 +1169,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1282,7 +1269,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1339,7 +1325,6 @@ describe("app controller", () => { isTTY: true, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1417,7 +1402,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1480,7 +1464,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1546,7 +1529,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1618,7 +1600,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1689,7 +1670,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1751,7 +1731,6 @@ describe("app controller", () => { flags: { yes: true }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1778,7 +1757,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1849,7 +1827,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -1915,7 +1892,6 @@ describe("app controller", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -2030,7 +2006,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -2180,7 +2155,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -2248,7 +2222,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -2329,7 +2302,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -2421,7 +2393,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -2481,7 +2452,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -2555,7 +2525,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -2664,7 +2633,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -2761,7 +2729,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -2901,7 +2868,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -2975,7 +2941,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_PROJECT_ID: "proj_123", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3045,7 +3010,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3108,7 +3072,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3132,7 +3095,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_PROJECT_ID: "proj_123", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3200,7 +3162,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); await context.stateStore.setAgentSetupPromptDismissedAt( @@ -3287,7 +3248,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3393,7 +3353,6 @@ describe("app controller", () => { isTTY: true, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3460,7 +3419,6 @@ describe("app controller", () => { isTTY: true, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3598,7 +3556,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3664,7 +3621,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3717,7 +3673,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3782,7 +3737,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3841,7 +3795,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3873,7 +3826,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3938,7 +3890,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -3973,7 +3924,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4001,7 +3951,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4065,7 +4014,6 @@ describe("app controller", () => { isTTY: true, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); await context.stateStore.setAgentSetupPromptDismissedAt( @@ -4140,7 +4088,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4204,7 +4151,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4276,7 +4222,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4381,7 +4326,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4476,7 +4420,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4606,7 +4549,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4673,7 +4615,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4731,7 +4672,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4805,7 +4745,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4891,7 +4830,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -4934,7 +4872,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5009,7 +4946,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5067,7 +5003,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5129,7 +5064,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5213,7 +5147,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5321,7 +5254,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5392,7 +5324,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5459,7 +5390,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5520,7 +5450,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5581,7 +5510,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5642,7 +5570,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5696,7 +5623,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5773,7 +5699,6 @@ describe("app controller", () => { isTTY: true, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5851,7 +5776,6 @@ describe("app controller", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5923,7 +5847,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -5994,7 +5917,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -6072,7 +5994,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -6161,7 +6082,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -6233,7 +6153,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -6315,7 +6234,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -6412,7 +6330,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -6510,7 +6427,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -6584,7 +6500,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -6647,7 +6562,6 @@ describe("app controller", () => { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "proj_456", PRISMA_CLI_TEST_REMEMBER_PROJECT_NAME: "Billing API", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -6745,7 +6659,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -6838,7 +6751,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -6910,7 +6822,6 @@ describe("app controller", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -7000,7 +6911,6 @@ describe("app controller", () => { env: { ...process.env, PRISMA_SERVICE_TOKEN: "token", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -7079,7 +6989,6 @@ describe("app controller", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -7165,7 +7074,6 @@ describe("app controller", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -7220,7 +7128,6 @@ describe("app controller", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -7300,7 +7207,6 @@ describe("app controller", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -7382,7 +7288,6 @@ describe("app controller", () => { isTTY: true, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -7444,7 +7349,6 @@ describe("app controller", () => { isTTY: false, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -7505,7 +7409,6 @@ describe("app controller", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -7566,7 +7469,6 @@ describe("app controller", () => { }, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -7641,7 +7543,6 @@ describe("app controller", () => { stateDir: path.join(cwd, ".state"), env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -7732,7 +7633,6 @@ describe("app controller", () => { stateDir: path.join(cwd, ".state"), env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); diff --git a/packages/cli/tests/app-env-vars.test.ts b/packages/cli/tests/app-env-vars.test.ts index 05872573..5a72c721 100644 --- a/packages/cli/tests/app-env-vars.test.ts +++ b/packages/cli/tests/app-env-vars.test.ts @@ -308,7 +308,6 @@ describe("app env vars", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -377,7 +376,6 @@ describe("app env vars", () => { env: { ...process.env, PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "", - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -445,7 +443,6 @@ describe("app env vars", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -532,7 +529,6 @@ describe("app env vars", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -655,7 +651,6 @@ describe("app env vars", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -783,7 +778,6 @@ describe("app env vars", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); diff --git a/packages/cli/tests/app.test.ts b/packages/cli/tests/app.test.ts index 3cccdbe1..5c5d69b6 100644 --- a/packages/cli/tests/app.test.ts +++ b/packages/cli/tests/app.test.ts @@ -4,8 +4,6 @@ import { describe, expect, it } from "vitest"; import { createTempCwd, executeCli } from "./helpers"; -const fixturePath = path.resolve("fixtures/mock-api.json"); - describe("app commands", () => { it("shows the documented help text for app commands and adds app to root help", async () => { const cwd = await createTempCwd(); @@ -15,115 +13,96 @@ describe("app commands", () => { argv: ["--help"], cwd, stateDir, - fixturePath, }); const appHelp = await executeCli({ argv: ["app", "--help"], cwd, stateDir, - fixturePath, }); const buildHelp = await executeCli({ argv: ["app", "build", "--help"], cwd, stateDir, - fixturePath, }); const runHelp = await executeCli({ argv: ["app", "run", "--help"], cwd, stateDir, - fixturePath, }); const deployHelp = await executeCli({ argv: ["app", "deploy", "--help"], cwd, stateDir, - fixturePath, }); const showHelp = await executeCli({ argv: ["app", "show", "--help"], cwd, stateDir, - fixturePath, }); const openHelp = await executeCli({ argv: ["app", "open", "--help"], cwd, stateDir, - fixturePath, }); const domainHelp = await executeCli({ argv: ["app", "domain", "--help"], cwd, stateDir, - fixturePath, }); const domainAddHelp = await executeCli({ argv: ["app", "domain", "add", "--help"], cwd, stateDir, - fixturePath, }); const domainShowHelp = await executeCli({ argv: ["app", "domain", "show", "--help"], cwd, stateDir, - fixturePath, }); const domainRemoveHelp = await executeCli({ argv: ["app", "domain", "remove", "--help"], cwd, stateDir, - fixturePath, }); const domainRetryHelp = await executeCli({ argv: ["app", "domain", "retry", "--help"], cwd, stateDir, - fixturePath, }); const domainWaitHelp = await executeCli({ argv: ["app", "domain", "wait", "--help"], cwd, stateDir, - fixturePath, }); const logsHelp = await executeCli({ argv: ["app", "logs", "--help"], cwd, stateDir, - fixturePath, }); const listDeploysHelp = await executeCli({ argv: ["app", "list-deploys", "--help"], cwd, stateDir, - fixturePath, }); const showDeployHelp = await executeCli({ argv: ["app", "show-deploy", "--help"], cwd, stateDir, - fixturePath, }); const promoteHelp = await executeCli({ argv: ["app", "promote", "--help"], cwd, stateDir, - fixturePath, }); const rollbackHelp = await executeCli({ argv: ["app", "rollback", "--help"], cwd, stateDir, - fixturePath, }); const removeHelp = await executeCli({ argv: ["app", "remove", "--help"], cwd, stateDir, - fixturePath, }); expect(rootHelp.exitCode).toBe(0); @@ -272,13 +251,11 @@ describe("app commands", () => { argv: ["app", "update-env"], cwd, stateDir, - fixturePath, }); const listEnv = await executeCli({ argv: ["app", "list-env"], cwd, stateDir, - fixturePath, }); expect(updateEnv.exitCode).not.toBe(0); @@ -295,7 +272,6 @@ describe("app commands", () => { argv: ["app", "deploy", "--db", "--no-db", "--yes"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(2); diff --git a/packages/cli/tests/auth-controller.test.ts b/packages/cli/tests/auth-controller.test.ts deleted file mode 100644 index f3370efa..00000000 --- a/packages/cli/tests/auth-controller.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import path from "node:path"; -import { describe, expect, it } from "vitest"; - -import { runAuthLogin } from "../src/controllers/auth"; -import { createTempCwd, createTestCommandContext } from "./helpers"; - -const fixturePath = path.resolve("fixtures/mock-api.json"); - -describe("auth controller", () => { - it("returns a structured usage error when login cannot prompt and selectors are missing", async () => { - const cwd = await createTempCwd(); - const stateDir = path.join(cwd, ".state"); - const { context } = await createTestCommandContext({ - cwd, - stateDir, - fixturePath, - isTTY: false, - }); - - await expect(runAuthLogin(context, {})).rejects.toMatchObject({ - code: "USAGE_ERROR", - domain: "auth", - summary: "Login requires explicit selectors in non-interactive mode", - }); - }); -}); diff --git a/packages/cli/tests/auth-presenter.test.ts b/packages/cli/tests/auth-presenter.test.ts new file mode 100644 index 00000000..12f5cc7e --- /dev/null +++ b/packages/cli/tests/auth-presenter.test.ts @@ -0,0 +1,104 @@ +/** + * Direct coverage for `renderAuthSuccess`. + * + * It used to be reached only through fixture-mode runs of `auth login` + * and `auth whoami`, so retiring that mode took its coverage with it. + * The rendering is real behaviour and users see it, so it is tested + * here on its own rather than through whichever command happens to + * call it. + */ +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { renderAuthSuccess } from "../src/presenters/auth"; +import { getCommandDescriptor } from "../src/shell/command-meta"; +import type { AuthStateResult } from "../src/types/auth"; +import { createTempCwd, createTestCommandContext } from "./helpers"; + +async function render( + command: "auth.login" | "auth.logout" | "auth.whoami", + result: AuthStateResult, +): Promise { + const cwd = await createTempCwd(); + const { context } = await createTestCommandContext({ + cwd, + stateDir: path.join(cwd, ".state"), + }); + return renderAuthSuccess( + context, + getCommandDescriptor(command), + command, + result, + ).join("\n"); +} + +const SIGNED_OUT: AuthStateResult = { + authenticated: false, + provider: null, + user: null, + workspace: null, + credential: null, +}; + +describe("renderAuthSuccess", () => { + it("shows provider, user and workspace after a login", async () => { + const output = await render("auth.login", { + authenticated: true, + provider: "github", + user: { email: "dev@example.com" }, + workspace: { id: "ws_1", name: "Acme Inc" }, + credential: null, + }); + + expect(output).toContain("GitHub"); + expect(output).toContain("dev@example.com"); + expect(output).toContain("Acme Inc"); + }); + + it("omits the rows it has no values for", async () => { + const output = await render("auth.login", { + ...SIGNED_OUT, + authenticated: true, + }); + + // No provider, user or workspace: those rows must be absent rather + // than rendered empty. + expect(output).not.toContain("provider"); + expect(output).not.toContain("user"); + expect(output).not.toContain("workspace"); + }); + + it("names a service token when there is no user email", async () => { + const named = await render("auth.whoami", { + authenticated: true, + provider: null, + user: null, + workspace: { id: "ws_1", name: "Acme Inc" }, + credential: { type: "service_token", id: "tok_1", name: "ci" }, + }); + expect(named).toContain(""); + + const anonymous = await render("auth.whoami", { + authenticated: true, + provider: null, + user: null, + workspace: null, + credential: { type: "service_token", id: "tok_1", name: null }, + }); + expect(anonymous).toContain(""); + }); + + it("reports signed out without identity rows", async () => { + const output = await render("auth.whoami", SIGNED_OUT); + + expect(output).toContain("signed out"); + expect(output).not.toContain("signed in"); + }); + + it("reports the cleared session on logout", async () => { + const output = await render("auth.logout", SIGNED_OUT); + + expect(output).toContain("local CLI state"); + }); +}); diff --git a/packages/cli/tests/auth-real-mode.test.ts b/packages/cli/tests/auth-real-mode.test.ts index 28f81509..c5bfaa5c 100644 --- a/packages/cli/tests/auth-real-mode.test.ts +++ b/packages/cli/tests/auth-real-mode.test.ts @@ -3,15 +3,9 @@ import path from "node:path"; import stripAnsi from "strip-ansi"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { MockApi } from "../src/adapters/mock-api"; -import { - renderAuthSuccess, - renderAuthWorkspaceList, -} from "../src/presenters/auth"; +import { renderAuthWorkspaceList } from "../src/presenters/auth"; import { getCommandDescriptor } from "../src/shell/command-meta"; -const fixturePath = path.resolve("fixtures/mock-api.json"); - afterEach(() => { vi.doUnmock("../src/auth/operations"); vi.doUnmock("@prisma/management-api-sdk"); @@ -57,11 +51,10 @@ describe("real auth mode", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); - const result = await runAuthLogin(context, {}); + const result = await runAuthLogin(context); expect(performLogin).toHaveBeenCalledWith( context.runtime.env, @@ -80,72 +73,6 @@ describe("real auth mode", () => { }); }); - it("stays in mock mode when fixture mode is enabled", async () => { - const performLogin = vi.fn().mockResolvedValue({ - token: "real-mode-access-token", - refreshToken: undefined, - expiresAt: undefined, - }); - const readAuthState = vi.fn().mockResolvedValue(null); - const performLogout = vi.fn().mockResolvedValue(undefined); - - vi.doMock("../src/auth/operations", async (importOriginal) => ({ - ...(await importOriginal()), - performLogin, - readAuthState, - performLogout, - })); - - const { createTempCwd, createTestCommandContext } = await import( - "./helpers" - ); - const { runAuthLogin } = await import("../src/controllers/auth"); - const cwd = await createTempCwd(); - const stateDir = path.join(cwd, ".state"); - const { context } = await createTestCommandContext({ - cwd, - stateDir, - fixturePath, - }); - - const result = await runAuthLogin(context, { - provider: "github", - user: "usr_456", - }); - - expect(performLogin).not.toHaveBeenCalled(); - expect(readAuthState).not.toHaveBeenCalled(); - expect(result.result).toMatchObject({ - authenticated: true, - provider: "github", - workspace: { - name: "Acme Inc", - }, - }); - }); - - it("does not eagerly load fixtures in real mode", async () => { - const loadSpy = vi.spyOn(MockApi, "load"); - const { createTempCwd, createTestCommandContext } = await import( - "./helpers" - ); - const cwd = await createTempCwd(); - const stateDir = path.join(cwd, ".state"); - const { context } = await createTestCommandContext({ - cwd, - stateDir, - env: { - ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, - }, - }); - - expect(loadSpy).not.toHaveBeenCalled(); - expect(() => context.api).toThrow( - "context.api accessed in real mode. Set runtime.fixturePath or PRISMA_CLI_MOCK_FIXTURE_PATH to use fixture mode.", - ); - }); - it("returns service-token identity in real auth JSON output", async () => { const readAuthState = vi.fn().mockResolvedValue({ authenticated: true, @@ -179,7 +106,6 @@ describe("real auth mode", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -241,7 +167,6 @@ describe("real auth mode", () => { const authFilePath = path.join(cwd, "auth.json"); const env = { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: "service-token", }; @@ -317,7 +242,6 @@ describe("real auth mode", () => { const authFilePath = path.join(cwd, "auth.json"); const env = { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: undefined, }; @@ -393,7 +317,6 @@ describe("real auth mode", () => { const authFilePath = path.join(cwd, "auth.json"); const env = { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: undefined, }; @@ -478,7 +401,6 @@ describe("real auth mode", () => { const authFilePath = path.join(cwd, "auth.json"); const env = { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: undefined, }; @@ -513,112 +435,4 @@ describe("real auth mode", () => { workspaceId: "cmmxworkspace2", }); }); - - it("omits empty provider and workspace rows in auth output", async () => { - const { createTempCwd, createTestCommandContext } = await import( - "./helpers" - ); - const cwd = await createTempCwd(); - const stateDir = path.join(cwd, ".state"); - const { context } = await createTestCommandContext({ - cwd, - stateDir, - fixturePath, - }); - - const output = renderAuthSuccess( - context, - getCommandDescriptor("auth.login"), - "auth.login", - { - authenticated: true, - provider: null, - user: { - email: "real@example.com", - }, - workspace: null, - credential: null, - }, - ).join(""); - - const plain = stripAnsi(output); - - expect(plain).toContain("user:"); - expect(plain).not.toContain("provider:"); - expect(plain).not.toContain("workspace:"); - }); - - it("omits the user row when a real auth state has no email", async () => { - const { createTempCwd, createTestCommandContext } = await import( - "./helpers" - ); - const cwd = await createTempCwd(); - const stateDir = path.join(cwd, ".state"); - const { context } = await createTestCommandContext({ - cwd, - stateDir, - fixturePath, - }); - - const output = renderAuthSuccess( - context, - getCommandDescriptor("auth.whoami"), - "auth.whoami", - { - authenticated: true, - provider: null, - user: null, - workspace: { - id: "ws_real", - name: "Real Workspace", - }, - credential: null, - }, - ).join(""); - - const plain = stripAnsi(output); - - expect(plain).toContain("status: signed in"); - expect(plain).not.toContain("user:"); - expect(plain).not.toContain("<>"); - }); - - it("shows service-token identity when no human user is present", async () => { - const { createTempCwd, createTestCommandContext } = await import( - "./helpers" - ); - const cwd = await createTempCwd(); - const stateDir = path.join(cwd, ".state"); - const { context } = await createTestCommandContext({ - cwd, - stateDir, - fixturePath, - }); - - const output = renderAuthSuccess( - context, - getCommandDescriptor("auth.whoami"), - "auth.whoami", - { - authenticated: true, - provider: null, - user: null, - workspace: { - id: "wksp_real", - name: "Real Workspace", - }, - credential: { - type: "service_token", - id: "itgr_ci", - name: "ci-deploys-prod", - }, - }, - ).join(""); - - const plain = stripAnsi(output); - - expect(plain).toContain("status: signed in"); - expect(plain).toContain("user: "); - expect(plain).toContain("workspace: Real Workspace"); - }); }); diff --git a/packages/cli/tests/auth-usecases.test.ts b/packages/cli/tests/auth-usecases.test.ts deleted file mode 100644 index b5460d96..00000000 --- a/packages/cli/tests/auth-usecases.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { createAuthUseCases } from "../src/use-cases/auth"; -import { createUseCaseGateways } from "./use-case-helpers"; - -describe("auth use cases", () => { - it("returns the signed-out empty state", async () => { - const { gateways } = await createUseCaseGateways(); - const useCases = createAuthUseCases(gateways); - - await expect(useCases.whoami()).resolves.toEqual({ - authenticated: false, - provider: null, - user: null, - workspace: null, - credential: null, - }); - }); - - it("persists login selection and returns the signed-in auth state", async () => { - const { gateways, readState } = await createUseCaseGateways(); - const useCases = createAuthUseCases(gateways); - - await expect( - useCases.login({ - provider: "github", - userId: "usr_456", - workspaceId: "ws_123", - }), - ).resolves.toEqual({ - authenticated: true, - provider: "github", - user: { - id: "usr_456", - email: "bob@example.com", - name: "Bob Example", - }, - workspace: { - id: "ws_123", - name: "Acme Inc", - }, - credential: { - type: "oauth", - id: null, - name: null, - }, - }); - - expect(readState().authSession).toEqual({ - provider: "github", - userId: "usr_456", - workspaceId: "ws_123", - }); - }); - - it("switches workspace by name case-insensitively", async () => { - const { gateways, readState } = await createUseCaseGateways({ - authSession: { - provider: "github", - userId: "usr_123", - workspaceId: "ws_123", - }, - }); - const useCases = createAuthUseCases(gateways); - - await expect(useCases.useWorkspace("prisma labs")).resolves.toMatchObject({ - workspace: { - id: "ws_456", - name: "Prisma Labs", - }, - }); - - expect(readState().authSession?.workspaceId).toBe("ws_456"); - }); - - it("clears the session on logout", async () => { - const { gateways, readState } = await createUseCaseGateways({ - authSession: { - provider: "github", - userId: "usr_456", - workspaceId: "ws_123", - }, - }); - const useCases = createAuthUseCases(gateways); - - await expect(useCases.logout()).resolves.toEqual({ - authenticated: false, - provider: null, - user: null, - workspace: null, - credential: null, - }); - - expect(readState().authSession).toBeNull(); - }); -}); diff --git a/packages/cli/tests/auth.test.ts b/packages/cli/tests/auth.test.ts index ef25e2cc..cb6979cc 100644 --- a/packages/cli/tests/auth.test.ts +++ b/packages/cli/tests/auth.test.ts @@ -6,8 +6,6 @@ import { describe, expect, it } from "vitest"; import { FileTokenStorage } from "../src/auth/token-storage"; import { createTempCwd, executeCli } from "./helpers"; -const fixturePath = path.resolve("fixtures/mock-api.json"); - async function writeAuthFile( authFilePath: string, tokens: unknown[], @@ -52,7 +50,6 @@ describe("auth commands", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: undefined, }, @@ -103,7 +100,6 @@ describe("auth commands", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: undefined, }, @@ -162,7 +158,6 @@ describe("auth commands", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: undefined, }, @@ -225,7 +220,6 @@ describe("auth commands", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: "service-token", }, @@ -269,7 +263,6 @@ describe("auth commands", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: "service-token", }, @@ -324,7 +317,6 @@ describe("auth commands", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: undefined, }, @@ -368,7 +360,6 @@ describe("auth commands", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: undefined, }, @@ -422,7 +413,6 @@ describe("auth commands", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: undefined, }, @@ -476,7 +466,6 @@ describe("auth commands", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_COMPUTE_AUTH_FILE: authFilePath, PRISMA_SERVICE_TOKEN: undefined, }, @@ -506,7 +495,6 @@ describe("auth commands", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_SERVICE_TOKEN: "service-token", }, }); @@ -532,7 +520,6 @@ describe("auth commands", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, PRISMA_SERVICE_TOKEN: " ", }, }); @@ -557,7 +544,6 @@ describe("auth commands", () => { argv: ["auth", "login", "--help"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(0); @@ -579,7 +565,6 @@ describe("auth commands", () => { argv: ["auth", "workspace", "--help"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(0); @@ -597,7 +582,6 @@ describe("auth commands", () => { argv: ["auth", "whoami"], cwd, stateDir, - fixturePath, isTTY: true, }); diff --git a/packages/cli/tests/branch-controller.test.ts b/packages/cli/tests/branch-controller.test.ts index fd3bec3d..a1c0b3e7 100644 --- a/packages/cli/tests/branch-controller.test.ts +++ b/packages/cli/tests/branch-controller.test.ts @@ -29,7 +29,7 @@ function createMockClient() { id: "proj_123", name: "Acme Dashboard", slug: "acme-dashboard", - workspace: { id: "ws_123", name: "Acme Inc" }, + workspace: { id: "wksp_ws_123", name: "Acme Inc" }, }, ], }, diff --git a/packages/cli/tests/branch-usecases.test.ts b/packages/cli/tests/branch-usecases.test.ts deleted file mode 100644 index 81da326a..00000000 --- a/packages/cli/tests/branch-usecases.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { createBranchUseCases } from "../src/use-cases/branch"; -import { createUseCaseGateways } from "./use-case-helpers"; - -describe("branch use cases", () => { - it("lists all resolved-project branches with role and env map metadata", async () => { - const { gateways } = await createUseCaseGateways({ - projectId: "proj_123", - }); - const useCases = createBranchUseCases(gateways); - - await expect(useCases.list()).resolves.toEqual({ - projectId: "proj_123", - projectName: "Acme Dashboard", - branches: [ - { - 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", - }, - ], - }); - }); -}); diff --git a/packages/cli/tests/database.test.ts b/packages/cli/tests/database.test.ts index 8101e768..1aa2355f 100644 --- a/packages/cli/tests/database.test.ts +++ b/packages/cli/tests/database.test.ts @@ -7,8 +7,6 @@ import { createTempCwd, executeCli } from "./helpers"; const DATABASE_HELP_ROW = /database\s+Manage Prisma Postgres databases for a project/; -const fixturePath = path.resolve("fixtures/mock-api.json"); - describe("database commands", () => { it("renders database and connection help without aliases or connection show", async () => { const cwd = await createTempCwd(); @@ -18,19 +16,16 @@ describe("database commands", () => { argv: ["--help"], cwd, stateDir, - fixturePath, }); const database = await executeCli({ argv: ["database", "--help"], cwd, stateDir, - fixturePath, }); const connection = await executeCli({ argv: ["database", "connection", "--help"], cwd, stateDir, - fixturePath, }); expect(root.exitCode).toBe(0); diff --git a/packages/cli/tests/e2e-coverage.test.ts b/packages/cli/tests/e2e-coverage.test.ts index 8f6a6ce1..bb635386 100644 --- a/packages/cli/tests/e2e-coverage.test.ts +++ b/packages/cli/tests/e2e-coverage.test.ts @@ -53,8 +53,7 @@ const EXCLUSIONS: Readonly> = { * * `service` and `build` act on a deployed service, which Composer * creates and this repo cannot; covering them needs a fixture service - * that outlives a CI run. `agent` writes local agent context files and - * should be straightforward to cover. + * that outlives a CI run. */ const AWAITING_COVERAGE: readonly string[] = [ "service show", @@ -70,9 +69,6 @@ const AWAITING_COVERAGE: readonly string[] = [ "service domain retry", "service domain wait", "build logs", - "agent install", - "agent update", - "agent status", ]; async function mountedCommands(): Promise { diff --git a/packages/cli/tests/helpers.ts b/packages/cli/tests/helpers.ts index 54b46d1b..84aee7aa 100644 --- a/packages/cli/tests/helpers.ts +++ b/packages/cli/tests/helpers.ts @@ -48,7 +48,6 @@ export async function executeCli(options: { argv: string[]; cwd?: string; env?: NodeJS.ProcessEnv; - fixturePath?: string; stateDir?: string; isTTY?: boolean; stdinText?: string; @@ -72,7 +71,6 @@ export async function executeCli(options: { cwd, env, signal: new AbortController().signal, - fixturePath: options.fixturePath, stateDir: options.stateDir, stdin: stdin as unknown as NodeJS.ReadStream, stdout: stdout as unknown as NodeJS.WriteStream, @@ -97,7 +95,6 @@ export async function createTestCommandContext(options: { argv?: string[]; cwd?: string; env?: NodeJS.ProcessEnv; - fixturePath?: string; stateDir?: string; isTTY?: boolean; stdinText?: string; @@ -127,7 +124,6 @@ export async function createTestCommandContext(options: { cwd: options.cwd ?? (await createTempCwd()), env: createTestEnv(options.env, options.preserveCI), signal: new AbortController().signal, - fixturePath: options.fixturePath, stateDir: options.stateDir, stdin: stdin as unknown as NodeJS.ReadStream, stdout: stdout as unknown as NodeJS.WriteStream, diff --git a/packages/cli/tests/helpers/fake-management-api.ts b/packages/cli/tests/helpers/fake-management-api.ts new file mode 100644 index 00000000..5d3ffbc6 --- /dev/null +++ b/packages/cli/tests/helpers/fake-management-api.ts @@ -0,0 +1,126 @@ +/** + * A real HTTP server standing in for the management API. + * + * This replaces the fixture mode the CLI used to carry in `src`. The + * difference matters: fixture mode was a second implementation *inside* + * the product, selected at runtime, so a test exercising it proved + * nothing about the code users run. Here the CLI runs its ordinary + * code — the same client, the same request pipeline, the same response + * parsing — and only the far end of the socket is ours. + * + * Ids are the shapes the API really returns, `wksp_`-prefixed workspace + * ids included. A fixture tidier than the API is how `project list` + * came to report "No projects found." for a workspace holding fifteen. + */ +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; + +export const FAKE_WORKSPACE_ID = "cmjs0z06102rz2mgzk5zqj495"; +export const FAKE_WORKSPACE_API_ID = `wksp_${FAKE_WORKSPACE_ID}`; + +export interface FakeProject { + readonly id: string; + readonly name: string; + readonly slug?: string; + readonly url?: string; +} + +export interface FakeManagementApi { + readonly baseUrl: string; + /** Every path the CLI asked for, in order. */ + readonly requests: readonly string[]; + close: () => Promise; +} + +export interface FakeManagementApiOptions { + readonly projects?: readonly FakeProject[]; + /** Extra routes, keyed by `" "`. Returning + * `undefined` falls through to the built-in routes. */ + readonly routes?: Record unknown>; +} + +const DEFAULT_PROJECTS: readonly FakeProject[] = [ + { id: "proj_123", name: "Acme Dashboard", slug: "acme-dashboard" }, + { id: "proj_456", name: "Billing API", slug: "billing-api" }, +]; + +export async function startFakeManagementApi( + options: FakeManagementApiOptions = {}, +): Promise { + const projects = options.projects ?? DEFAULT_PROJECTS; + const requests: string[] = []; + + const server: Server = createServer((req, res) => { + const url = new URL(req.url ?? "/", "http://localhost"); + const route = `${req.method} ${url.pathname}`; + requests.push(route); + + const custom = options.routes?.[route]; + const body = custom?.() ?? builtInRoute(route, url, projects); + + res.setHeader("Content-Type", "application/json"); + if (body === undefined) { + res.statusCode = 404; + res.end(JSON.stringify({ error: { message: `no route ${route}` } })); + return; + } + res.statusCode = 200; + res.end(JSON.stringify(body)); + }); + + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const { port } = server.address() as AddressInfo; + + return { + baseUrl: `http://127.0.0.1:${port}`, + requests, + close: () => + new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }), + }; +} + +function builtInRoute( + route: string, + url: URL, + projects: readonly FakeProject[], +): unknown { + // What the CLI asks before it trusts a credential. + if (route === "GET /v1/me") { + return { + data: { + user: { id: "usr_456", email: "dev@example.com", name: "Dev" }, + workspace: { id: FAKE_WORKSPACE_API_ID, name: "Acme Inc" }, + credential: { type: "service_token", id: "tok_1", name: "e2e" }, + }, + }; + } + + if (route === "GET /v1/projects") { + return { + data: projects.map((project) => ({ + ...project, + workspace: { id: FAKE_WORKSPACE_API_ID, name: "Acme Inc" }, + })), + }; + } + + const project = projects.find( + (candidate) => url.pathname === `/v1/projects/${candidate.id}`, + ); + if (project && route.startsWith("GET ")) { + return { + data: { + ...project, + workspace: { id: FAKE_WORKSPACE_API_ID, name: "Acme Inc" }, + }, + }; + } + + if (url.pathname === `/v1/workspaces/${FAKE_WORKSPACE_ID}`) { + return { data: { id: FAKE_WORKSPACE_API_ID, name: "Acme Inc" } }; + } + + return undefined; +} diff --git a/packages/cli/tests/init-agent-setup.test.ts b/packages/cli/tests/init-agent-setup.test.ts index dea390b8..4dd05c1d 100644 --- a/packages/cli/tests/init-agent-setup.test.ts +++ b/packages/cli/tests/init-agent-setup.test.ts @@ -64,7 +64,6 @@ async function setupInitAgentPromptTest(options: { flags: options.quiet ? { quiet: true } : undefined, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); diff --git a/packages/cli/tests/init.test.ts b/packages/cli/tests/init.test.ts index 8e02f5a4..e528085f 100644 --- a/packages/cli/tests/init.test.ts +++ b/packages/cli/tests/init.test.ts @@ -1,5 +1,6 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; +import { mintTestJwt } from "@prisma/cli-engine/testing"; import { COMPUTE_CONFIG_JSON_SCHEMA_URL, loadComputeConfig, @@ -8,16 +9,35 @@ import stripAnsi from "strip-ansi"; import { describe, expect, it } from "vitest"; import { createTempCwd, executeCli } from "./helpers"; +import { + FAKE_WORKSPACE_ID, + type FakeManagementApi, + startFakeManagementApi, +} from "./helpers/fake-management-api"; + +/** + * Authenticates the way the product actually supports without a browser: + * a service token in the environment. The old helper drove `auth login + * --provider --user`, a selection flow that existed only in fixture + * mode and went with it. + */ +const SERVICE_TOKEN = mintTestJwt({ + sub: "usr_456", + workspace_id: FAKE_WORKSPACE_ID, +}); -const fixturePath = path.resolve("fixtures/mock-api.json"); +async function login(_cwd: string, _stateDir: string) { + // Nothing to store: the credential travels in the environment that + // `initEnv` puts on every run below. +} -async function login(cwd: string, stateDir: string) { - await executeCli({ - argv: ["auth", "login", "--provider", "github", "--user", "usr_456"], - cwd, - stateDir, - fixturePath, - }); +function initEnv(api: FakeManagementApi): NodeJS.ProcessEnv { + return { + ...process.env, + PRISMA_SERVICE_TOKEN: SERVICE_TOKEN, + PRISMA_WORKSPACE_ID: FAKE_WORKSPACE_ID, + PRISMA_MANAGEMENT_API_URL: api.baseUrl, + }; } async function writePackageJson( @@ -56,7 +76,6 @@ describe("init", () => { ], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -102,7 +121,6 @@ describe("init", () => { argv: ["init", "--json"], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -128,7 +146,6 @@ describe("init", () => { argv: ["init", "--json"], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -153,7 +170,6 @@ describe("init", () => { argv: ["init", "--framework", "hono", "--json"], cwd, stateDir, - fixturePath, }); const directPayload = JSON.parse(direct.stdout); @@ -171,7 +187,6 @@ describe("init", () => { argv: ["init", "--framework", "hono", "--json"], cwd: nested, stateDir, - fixturePath, }); const nestedPayload = JSON.parse(fromNested.stdout); @@ -189,7 +204,7 @@ describe("init", () => { ["init", "--framework", "rails", "--json"], ]) { // biome-ignore lint/performance/noAwaitInLoops: all three runs share one cwd and state directory, and the assertion after the loop is that none of them wrote a config — overlapping runs could not tell you that. - const result = await executeCli({ argv, cwd, stateDir, fixturePath }); + const result = await executeCli({ argv, cwd, stateDir }); const payload = JSON.parse(result.stdout); expect(result.exitCode).toBe(2); expect(payload.error.code).toBe("USAGE_ERROR"); @@ -199,6 +214,7 @@ describe("init", () => { }); it("links to an explicit --project and reports it", async () => { + const api = await startFakeManagementApi(); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); await writePackageJson(cwd, { name: "api" }); @@ -208,7 +224,7 @@ describe("init", () => { argv: ["init", "--framework", "hono", "--project", "proj_123", "--json"], cwd, stateDir, - fixturePath, + env: initEnv(api), }); const payload = JSON.parse(result.stdout); @@ -224,9 +240,11 @@ describe("init", () => { await expect( readFile(path.join(cwd, ".prisma/local.json"), "utf8"), ).resolves.toContain("proj_123"); + await api.close(); }); it("downgrades a failed link to a warning and keeps the config", async () => { + const api = await startFakeManagementApi(); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); await writePackageJson(cwd, { name: "api" }); @@ -236,7 +254,7 @@ describe("init", () => { argv: ["init", "--framework", "hono", "--project", "nope", "--json"], cwd, stateDir, - fixturePath, + env: initEnv(api), }); const payload = JSON.parse(result.stdout); @@ -247,6 +265,7 @@ describe("init", () => { "npx -y @prisma/cli@latest project link", ); await expect(readConfig(cwd)).resolves.toContain('framework: "hono"'); + await api.close(); }); it("reports already-linked directories without prompting", async () => { @@ -263,7 +282,6 @@ describe("init", () => { argv: ["init", "--framework", "hono", "--json"], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -284,7 +302,6 @@ describe("init", () => { argv: ["init", "--framework", "hono", "--no-link"], cwd, stateDir, - fixturePath, }); const stderr = stripAnsi(result.stderr); @@ -304,7 +321,6 @@ describe("init", () => { argv: ["init", "--framework", "custom", "--no-link", "--json"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(0); @@ -327,7 +343,6 @@ describe("init types install", () => { argv: ["init", "--framework", "hono", "--no-link", "--json"], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -358,7 +373,6 @@ describe("init types install", () => { ], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -376,7 +390,6 @@ describe("init types install", () => { argv: ["init", "--framework", "hono", "--install", "--no-link", "--json"], cwd, stateDir, - fixturePath, env: { PRISMA_CLI_INIT_INSTALL_COMMAND: JSON.stringify([ "node", @@ -401,7 +414,6 @@ describe("init types install", () => { argv: ["init", "--framework", "hono", "--install", "--no-link", "--json"], cwd, stateDir, - fixturePath, env: { PRISMA_CLI_INIT_INSTALL_COMMAND: JSON.stringify([ "node", @@ -441,7 +453,6 @@ describe("init config format", () => { ], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -510,7 +521,6 @@ describe("init config format", () => { ], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -540,7 +550,6 @@ describe("init config format", () => { ], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -559,7 +568,6 @@ describe("init config format", () => { argv: ["init", "--framework", "hono", "--format", "yaml", "--json"], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -583,7 +591,6 @@ describe("init config format", () => { ], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -607,7 +614,6 @@ describe("init config format", () => { argv: ["init", "--framework", "hono", "--format", "json", "--json"], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -637,7 +643,7 @@ describe("init config format", () => { ["init", "--framework", "hono", "--format", "json", "--json"], ]) { // biome-ignore lint/performance/noAwaitInLoops: both runs share one cwd holding the prisma.compute.json they must refuse to overwrite, so the second run has to see the file the first one left alone. - const result = await executeCli({ argv, cwd, stateDir, fixturePath }); + const result = await executeCli({ argv, cwd, stateDir }); const payload = JSON.parse(result.stdout); expect(result.exitCode).toBe(1); expect(payload.error.code).toBe("INIT_CONFIG_EXISTS"); @@ -672,7 +678,6 @@ describe("init config format", () => { argv: ["init", "--format", "ts", "--json"], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -740,7 +745,6 @@ describe("init config format", () => { argv: ["init", "--format", "ts", "--json"], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -796,7 +800,6 @@ describe("init config format", () => { argv: ["init", "--format", "ts", "--json"], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -849,7 +852,6 @@ describe("init config format", () => { ], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -876,7 +878,6 @@ describe("init config format", () => { argv: ["init", "--format", "ts", "--install", "--json"], cwd, stateDir, - fixturePath, env: { PRISMA_CLI_INIT_INSTALL_COMMAND: JSON.stringify([ "node", @@ -895,6 +896,7 @@ describe("init config format", () => { }); it("honors link flags when converting, like fresh init", async () => { + const api = await startFakeManagementApi(); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); await writePackageJson(cwd, { name: "api" }); @@ -914,7 +916,7 @@ describe("init config format", () => { ], cwd, stateDir, - fixturePath, + env: initEnv(api), }); const linkedPayload = JSON.parse(linked.stdout); @@ -936,14 +938,15 @@ describe("init config format", () => { argv: ["init", "--format", "ts", "--no-link", "--no-install", "--json"], cwd: cwd2, stateDir, - fixturePath, }); const skippedPayload = JSON.parse(skipped.stdout); expect(skipped.exitCode).toBe(0); expect(skippedPayload.result.link.status).toBe("skipped"); + await api.close(); }); it("runs conversion side effects in the config directory, not the invocation directory", async () => { + const api = await startFakeManagementApi(); const cwd = await createTempCwd(); const stateDir = path.join(cwd, ".state"); await mkdir(path.join(cwd, ".git"), { recursive: true }); @@ -969,8 +972,8 @@ describe("init config format", () => { ], cwd: nested, stateDir, - fixturePath, env: { + ...initEnv(api), // The fake installer records its working directory on disk. PRISMA_CLI_INIT_INSTALL_COMMAND: JSON.stringify([ "node", @@ -1011,6 +1014,7 @@ describe("init config format", () => { await expect(readJsonConfig(cwd)).rejects.toMatchObject({ code: "ENOENT", }); + await api.close(); }); it("prints the human conversion summary", async () => { @@ -1025,7 +1029,6 @@ describe("init config format", () => { argv: ["init", "--format", "ts", "--no-install"], cwd, stateDir, - fixturePath, }); const stderr = stripAnsi(result.stderr); @@ -1053,7 +1056,6 @@ describe("init edge cases", () => { ], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); @@ -1086,7 +1088,6 @@ describe("init edge cases", () => { ], cwd, stateDir, - fixturePath, }); const payload = JSON.parse(result.stdout); diff --git a/packages/cli/tests/project-real-mode.test.ts b/packages/cli/tests/project-real-mode.test.ts index bfba7406..8551fa14 100644 --- a/packages/cli/tests/project-real-mode.test.ts +++ b/packages/cli/tests/project-real-mode.test.ts @@ -52,14 +52,14 @@ function mockClient( name: "Billing API", slug: "billing-api", url: "https://prisma.build/acme/billing-api", - workspace: { id: "ws_123", name: "Acme Inc" }, + workspace: { id: "wksp_ws_123", name: "Acme Inc" }, }, { id: "proj_123", name: "Acme Dashboard", slug: "acme-dashboard", url: "https://prisma.build/acme/acme-dashboard", - workspace: { id: "ws_123", name: "Acme Inc" }, + workspace: { id: "wksp_ws_123", name: "Acme Inc" }, }, ], }, @@ -153,7 +153,6 @@ describe("real project mode", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -220,7 +219,6 @@ describe("real project mode", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -334,7 +332,6 @@ describe("real project mode", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -404,7 +401,6 @@ describe("real project mode", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); @@ -480,7 +476,6 @@ describe("real project mode", () => { stateDir, env: { ...process.env, - PRISMA_CLI_MOCK_FIXTURE_PATH: undefined, }, }); diff --git a/packages/cli/tests/project-usecases.test.ts b/packages/cli/tests/project-usecases.test.ts deleted file mode 100644 index fba604f8..00000000 --- a/packages/cli/tests/project-usecases.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { createProjectUseCases } from "../src/use-cases/project"; -import { createUseCaseGateways } from "./use-case-helpers"; - -describe("project use cases", () => { - it("lists sorted projects for the authenticated workspace", async () => { - const { gateways } = await createUseCaseGateways(); - const useCases = createProjectUseCases(gateways); - - await expect( - useCases.list({ - authenticated: true, - provider: "github", - user: { - email: "bob@example.com", - }, - workspace: { - id: "ws_123", - name: "Acme Inc", - }, - credential: null, - }), - ).resolves.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_999", - name: "Sandbox", - url: "https://prisma.build/acme/sandbox", - }, - ], - }); - }); -}); diff --git a/packages/cli/tests/project.test.ts b/packages/cli/tests/project.test.ts index 6e01a3b9..91c8842a 100644 --- a/packages/cli/tests/project.test.ts +++ b/packages/cli/tests/project.test.ts @@ -4,8 +4,6 @@ import { describe, expect, it } from "vitest"; import { createTempCwd, executeCli } from "./helpers"; -const fixturePath = path.resolve("fixtures/mock-api.json"); - describe("project commands", () => { it("shows Public Beta project, setup, and git help", async () => { const cwd = await createTempCwd(); @@ -15,43 +13,36 @@ describe("project commands", () => { argv: ["project", "--help"], cwd, stateDir, - fixturePath, }); const showHelp = await executeCli({ argv: ["project", "show", "--help"], cwd, stateDir, - fixturePath, }); const createHelp = await executeCli({ argv: ["project", "create", "--help"], cwd, stateDir, - fixturePath, }); const linkHelp = await executeCli({ argv: ["project", "link", "--help"], cwd, stateDir, - fixturePath, }); const gitHelp = await executeCli({ argv: ["git", "--help"], cwd, stateDir, - fixturePath, }); const connectRepoHelp = await executeCli({ argv: ["git", "connect", "--help"], cwd, stateDir, - fixturePath, }); const disconnectRepoHelp = await executeCli({ argv: ["git", "disconnect", "--help"], cwd, 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}`, @@ -86,13 +77,11 @@ describe("project commands", () => { argv: ["project", "env", "remove", "--help"], cwd, stateDir, - fixturePath, }); const rmHelp = await executeCli({ argv: ["project", "env", "rm", "--help"], cwd, stateDir, - fixturePath, }); expect(removeHelp.exitCode).toBe(0); diff --git a/packages/cli/tests/shell.test.ts b/packages/cli/tests/shell.test.ts index 50151fb8..b0724191 100644 --- a/packages/cli/tests/shell.test.ts +++ b/packages/cli/tests/shell.test.ts @@ -13,8 +13,6 @@ import { import { maskValue } from "../src/shell/ui"; import { createTempCwd, executeCli } from "./helpers"; -const fixturePath = path.resolve("fixtures/mock-api.json"); - describe("shell behavior", () => { it("formats command arguments for shell-safe next steps", () => { expect(formatCommandArgument("billing-api")).toBe("billing-api"); @@ -121,7 +119,6 @@ describe("shell behavior", () => { argv: ["--help"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(0); @@ -162,31 +159,26 @@ describe("shell behavior", () => { argv: [], cwd, stateDir, - fixturePath, }); const authResult = await executeCli({ argv: ["auth"], cwd, stateDir, - fixturePath, }); const projectResult = await executeCli({ argv: ["project"], cwd, stateDir, - fixturePath, }); const branchResult = await executeCli({ argv: ["branch"], cwd, stateDir, - fixturePath, }); const databaseResult = await executeCli({ argv: ["database"], cwd, stateDir, - fixturePath, }); expect(rootResult.exitCode).toBe(0); @@ -231,7 +223,6 @@ describe("shell behavior", () => { argv: ["--json", "auth", "whoami"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(0); @@ -253,7 +244,6 @@ describe("shell behavior", () => { argv: ["auth", "--quiet", "whoami"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(0); @@ -269,7 +259,6 @@ describe("shell behavior", () => { argv: ["--no-interactive", "project", "show"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(1); @@ -284,7 +273,6 @@ describe("shell behavior", () => { argv: ["auth", "logni"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(2); @@ -300,7 +288,6 @@ describe("shell behavior", () => { argv: ["auth", "whoami", "--quiet"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(0); @@ -316,7 +303,6 @@ describe("shell behavior", () => { argv: ["project", "show", "--no-interactive", "--trace"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(1); @@ -332,7 +318,6 @@ describe("shell behavior", () => { argv: ["auth", "whoami"], cwd, stateDir, - fixturePath, isTTY: true, env: { ...process.env, @@ -352,7 +337,6 @@ describe("shell behavior", () => { argv: ["auth", "whoami", "--color"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(0); diff --git a/packages/cli/tests/update-check.test.ts b/packages/cli/tests/update-check.test.ts index 2b9a43e9..9ced398b 100644 --- a/packages/cli/tests/update-check.test.ts +++ b/packages/cli/tests/update-check.test.ts @@ -10,8 +10,6 @@ import { } from "../src/update-check"; import { createTempCwd, executeCli } from "./helpers"; -const fixturePath = path.resolve("fixtures/mock-api.json"); - describe("automatic update check", () => { it("prints a cached update notice to stderr before eligible command output", async () => { const { cwd, stateDir, updateCheckDir } = await createUpdateCheckTestDirs(); @@ -21,7 +19,6 @@ describe("automatic update check", () => { argv: ["auth", "whoami"], cwd, stateDir, - fixturePath, isTTY: true, env: enableUpdateCheck(updateCheckDir), }); @@ -47,7 +44,6 @@ describe("automatic update check", () => { argv: ["project", "show", "--no-interactive"], cwd, stateDir, - fixturePath, isTTY: true, env: enableUpdateCheck(updateCheckDir), }); @@ -66,7 +62,6 @@ describe("automatic update check", () => { argv: ["--json", "auth", "whoami"], cwd, stateDir, - fixturePath, isTTY: true, env: enableUpdateCheck(updateCheckDir), }); @@ -129,7 +124,6 @@ describe("automatic update check", () => { argv, cwd, stateDir, - fixturePath, isTTY, preserveCI, env: { @@ -150,7 +144,6 @@ describe("automatic update check", () => { argv: ["auth", "whoami"], cwd, stateDir, - fixturePath, isTTY: true, env: enableUpdateCheck(updateCheckDir), }); @@ -174,7 +167,6 @@ describe("automatic update check", () => { argv: ["auth", "whoami"], cwd, stateDir, - fixturePath, isTTY: true, env: enableUpdateCheck(updateCheckDir), }); @@ -197,7 +189,6 @@ describe("automatic update check", () => { argv: ["auth", "whoami"], cwd, stateDir, - fixturePath, isTTY: true, env, }); @@ -205,7 +196,6 @@ describe("automatic update check", () => { argv: ["auth", "whoami"], cwd, stateDir, - fixturePath, isTTY: true, env, }); @@ -221,7 +211,6 @@ describe("automatic update check", () => { argv: ["auth", "whoami"], cwd, stateDir, - fixturePath, isTTY: true, env: enableUpdateCheck(updateCheckDir), }); @@ -243,7 +232,6 @@ describe("automatic update check", () => { argv: ["--json", "auth", "whoami"], cwd, stateDir, - fixturePath, isTTY: true, env: enableUpdateCheck(updateCheckDir), }); @@ -266,7 +254,6 @@ describe("automatic update check", () => { argv: ["auth", "whoami"], cwd, stateDir, - fixturePath, isTTY: true, env: enableUpdateCheck(updateCheckDir), }); diff --git a/packages/cli/tests/use-case-helpers.ts b/packages/cli/tests/use-case-helpers.ts deleted file mode 100644 index 6388a7ec..00000000 --- a/packages/cli/tests/use-case-helpers.ts +++ /dev/null @@ -1,97 +0,0 @@ -import path from "node:path"; - -import { MockApi } from "../src/adapters/mock-api"; -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"); - -export async function createUseCaseGateways(options?: { - authSession?: AuthSessionRecord | null; - projectId?: string | null; -}): Promise<{ - gateways: CliUseCaseGateways; - readState: () => { - authSession: AuthSessionRecord | null; - projectId: string | null; - }; -}> { - const api = await MockApi.load(fixturePath); - let authSession = options?.authSession ?? null; - const projectId = options?.projectId ?? null; - - return { - gateways: { - identityGateway: { - listProviders: () => api.listProviders(), - getProvider: (providerId) => api.getProvider(providerId), - listUsersForProvider: (providerId) => - api.listUsersForProvider(providerId).map(toAuthUser), - getUser: (userId) => { - const user = api.getUser(userId); - return user ? toAuthUser(user) : undefined; - }, - getUserForProvider: (providerId, userId) => { - const user = api.getUserForProvider(providerId, userId); - return user ? toAuthUser(user) : undefined; - }, - listUserWorkspaces: (userId) => - api.listUserWorkspaces(userId).map(toAuthWorkspace), - getWorkspace: (workspaceId) => { - const workspace = api.getWorkspace(workspaceId); - return workspace ? toAuthWorkspace(workspace) : undefined; - }, - getUserWorkspace: (userId, workspaceId) => { - const workspace = api.getUserWorkspace(userId, workspaceId); - return workspace ? toAuthWorkspace(workspace) : undefined; - }, - }, - projectGateway: { - listProjectsForWorkspace: (workspaceId) => - api.listProjectsForWorkspace(workspaceId), - getProject: (projectId) => api.getProject(projectId), - getProjectForWorkspace: (workspaceId, projectId) => - api.getProjectForWorkspace(workspaceId, projectId), - }, - branchGateway: { - listBranchesForProject: (projectId) => - api.listBranchesForProject(projectId), - getBranchForProject: (projectId, name) => { - return api.getBranchForProject(projectId, name); - }, - getDeployment: (deploymentId) => api.getDeployment(deploymentId), - }, - projectStateGateway: { - readRememberedProjectId: async () => projectId, - }, - sessionGateway: { - readAuthSession: async () => authSession, - writeAuthSession: async (session) => { - authSession = session; - }, - clearAuthSession: async () => { - authSession = null; - }, - }, - }, - readState: () => ({ - authSession, - projectId, - }), - }; -} - -function toAuthUser(user: { id: string; name: string; email: string }) { - return { - id: user.id, - name: user.name, - email: user.email, - }; -} - -function toAuthWorkspace(workspace: { id: string; name: string }) { - return { - id: workspace.id, - name: workspace.name, - }; -} diff --git a/packages/cli/tests/v8-branch.test.ts b/packages/cli/tests/v8-branch.test.ts index 4f83e779..4d1ba49a 100644 --- a/packages/cli/tests/v8-branch.test.ts +++ b/packages/cli/tests/v8-branch.test.ts @@ -21,7 +21,7 @@ const PROJECTS = [ { id: "proj_1", name: "Billing", - workspace: { id: "ws_1", name: "Acme Inc" }, + workspace: { id: "wksp_ws_1", name: "Acme Inc" }, }, ]; diff --git a/packages/cli/tests/v8-bucket.test.ts b/packages/cli/tests/v8-bucket.test.ts index 91f45e9d..d1a0bbf8 100644 --- a/packages/cli/tests/v8-bucket.test.ts +++ b/packages/cli/tests/v8-bucket.test.ts @@ -26,7 +26,7 @@ const PROJECTS = [ { id: "proj_1", name: "Billing", - workspace: { id: "ws_1", name: "Acme Inc" }, + workspace: { id: "wksp_ws_1", name: "Acme Inc" }, }, ]; diff --git a/packages/cli/tests/v8-git.test.ts b/packages/cli/tests/v8-git.test.ts index f16a55ba..0cd9a9f3 100644 --- a/packages/cli/tests/v8-git.test.ts +++ b/packages/cli/tests/v8-git.test.ts @@ -30,7 +30,7 @@ const PROJECTS = [ { id: "proj_1", name: "Billing", - workspace: { id: "ws_1", name: "Acme Inc" }, + workspace: { id: "wksp_ws_1", name: "Acme Inc" }, }, ]; diff --git a/packages/cli/tests/v8-legacy-context.test.ts b/packages/cli/tests/v8-legacy-context.test.ts index 6ed687c4..5d52ccc9 100644 --- a/packages/cli/tests/v8-legacy-context.test.ts +++ b/packages/cli/tests/v8-legacy-context.test.ts @@ -64,8 +64,8 @@ describe("the v8 legacy-context adapter", () => { it("refuses a runtime field it cannot serve, and names it", () => { const context = legacyOperationContext(stubContext("/somewhere")); - expect(() => context.runtime.fixturePath).toThrow( - "the v8 legacy-context adapter provides only runtime.cwd, runtime.env and runtime.signal; runtime.fixturePath was read", + expect(() => context.runtime.stdout).toThrow( + "the v8 legacy-context adapter provides only runtime.cwd, runtime.env and runtime.signal; runtime.stdout was read", ); }); diff --git a/packages/cli/tests/v8-postgres.test.ts b/packages/cli/tests/v8-postgres.test.ts index 05f0e38e..db1989f7 100644 --- a/packages/cli/tests/v8-postgres.test.ts +++ b/packages/cli/tests/v8-postgres.test.ts @@ -31,7 +31,7 @@ const PROJECTS = [ { id: "proj_1", name: "Billing", - workspace: { id: "ws_1", name: "Acme Inc" }, + workspace: { id: "wksp_ws_1", name: "Acme Inc" }, }, ]; diff --git a/packages/cli/tests/v8-project.test.ts b/packages/cli/tests/v8-project.test.ts index b63f9f44..c3f89914 100644 --- a/packages/cli/tests/v8-project.test.ts +++ b/packages/cli/tests/v8-project.test.ts @@ -35,6 +35,16 @@ beforeEach(() => { vi.mocked(resolveRecipientWorkspaceSession).mockReset(); }); +/** + * The two ids one workspace has. A credential's `workspace_id` claim + * carries the bare form; the management API answers with the same id + * behind a `wksp_` prefix. Fixtures that used one string for both could + * not see the mismatch that made `project list` report "No projects + * found." for a workspace full of projects. + */ +const WORKSPACE_ID = "ws_1"; +const API_WORKSPACE_ID = `wksp_${WORKSPACE_ID}`; + const ACME_SESSION = { workspaceId: "ws_1", workspaceName: "Acme Inc", @@ -54,13 +64,13 @@ const API_PROJECTS = [ id: "proj_1", name: "Billing", defaultRegion: "us-east-1", - workspace: { id: "ws_1", name: "Acme Inc" }, + workspace: { id: API_WORKSPACE_ID, name: "Acme Inc" }, }, { id: "proj_2", name: "Storefront", defaultRegion: null, - workspace: { id: "ws_1", name: "Acme Inc" }, + workspace: { id: API_WORKSPACE_ID, name: "Acme Inc" }, }, ]; @@ -164,7 +174,7 @@ describe("prisma-v8 project list", () => { expect(result.exitCode).toBe(0); expect(result.presented?.data).toMatchObject({ - workspace: { id: "ws_1", name: "Acme Inc" }, + workspace: { id: WORKSPACE_ID, name: "Acme Inc" }, localBinding: { status: "linked" }, projects: [ { id: "proj_1", name: "Billing", defaultRegion: "us-east-1" }, @@ -491,7 +501,7 @@ describe("prisma-v8 project show", () => { ok: true, commandId: "project.show", result: { - workspace: { id: "ws_1", name: "Acme Inc" }, + workspace: { id: WORKSPACE_ID, name: "Acme Inc" }, project: { id: "proj_2", name: "Storefront" }, resolution: { projectSource: "local-pin", targetName: "Storefront" }, }, @@ -665,7 +675,7 @@ describe("prisma-v8 project create", () => { ok: true, commandId: "project.create", result: { - workspace: { id: "ws_1", name: "Acme Inc" }, + workspace: { id: WORKSPACE_ID, name: "Acme Inc" }, project: { id: "proj_new", name: "my-app" }, localPin: { path: ".prisma/local.json", written: true }, action: "created", @@ -2671,7 +2681,7 @@ describe("prisma-v8 project remove", () => { ok: true, commandId: "project.remove", result: { - workspace: { id: "ws_1", name: "Acme Inc" }, + workspace: { id: WORKSPACE_ID, name: "Acme Inc" }, project: { id: "proj_1", name: "Billing" }, localPin: { cleared: false }, }, diff --git a/packages/cli/tests/version.test.ts b/packages/cli/tests/version.test.ts index 78732ab5..e27525d9 100644 --- a/packages/cli/tests/version.test.ts +++ b/packages/cli/tests/version.test.ts @@ -10,7 +10,6 @@ import { createTempCwd, executeCli } from "./helpers"; const requireFromHere = createRequire(import.meta.url); const pkg = requireFromHere("../package.json") as { version: string }; -const fixturePath = path.resolve("fixtures/mock-api.json"); describe("version", () => { it("prints the CLI version to stdout when --version is passed", async () => { @@ -152,7 +151,6 @@ describe("version", () => { argv: ["--help"], cwd, stateDir, - fixturePath, }); expect(result.exitCode).toBe(0);