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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/product/command-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,55 @@ prisma-cli git disconnect --project proj_123
prisma-cli git disconnect --json
```

## `prisma-cli git account list`

Purpose:

- show the GitHub accounts connected to the active workspace, and which accounts can be connected without reinstalling

Behavior:

- requires auth
- lists connected accounts with their login, account type (`user` or `organization`), numeric installation id, and suspension state
- lists connectable accounts: GitHub accounts already installed via other workspaces the user belongs to, deduplicated per account with the newest installation winning, so they can be connected here without a GitHub round trip
- suggests `git account connect` as a next step

Examples:

```bash
prisma-cli git account list
prisma-cli git account list --json
```

## `prisma-cli git account connect [account]`

Purpose:

- add a GitHub account to the active workspace, either by connecting one already installed elsewhere or by installing the Prisma GitHub App on a new account

Behavior:

- requires auth
- with `[account]` (login or numeric installation id): connects that account directly, no prompt. This is the path agents and CI use.
- without `[account]` in an interactive terminal: shows a picker of connectable accounts plus a `+ Install a new GitHub account` option. Choosing an account connects it; choosing install opens GitHub in the browser and waits for the install to finish, so the command never dead-ends without a result.
- without `[account]` in a non-interactive context (agent, CI, `--json`): never prompts. Fails with `GIT_ACCOUNT_REQUIRED`, carrying the connectable accounts in `error.meta.connectable` (and the install URL in `error.meta.installUrl` when nothing is connectable) plus runnable next steps, so the caller can pick without blocking.
- an unknown account fails with `GIT_ACCOUNT_NOT_FOUND` and the same machine-readable options
- authorization is enforced by the platform: a full user session whose user is a member of a workspace the account is already actively connected to
- if the browser install does not complete before the wait times out, fails with `GIT_INSTALL_TIMED_OUT`; the install may still finish, so re-check with `git account list`
- if GitHub reports the installation gone, fails with `GIT_CONNECT_FAILED` after the platform cleans up its stale records
- this connects the account (organization/user) to the workspace; link a specific repository to a project with `git connect`

Examples:

```bash
# Interactive: pick a connectable account or install a new one
prisma-cli git account connect

# Non-interactive: connect a specific account
prisma-cli git account connect acme-org
prisma-cli git account connect 555003 --json
```

## `prisma-cli branch list`

Purpose:
Expand Down
8 changes: 8 additions & 0 deletions docs/product/error-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ These codes are the minimum stable set for the MVP:
- `LOCAL_STATE_WRITE_FAILED`
- `LOCAL_STATE_STALE`
- `BRANCH_NOT_DEPLOYABLE`
- `GIT_ACCOUNT_REQUIRED`
- `GIT_ACCOUNT_NOT_FOUND`
- `GIT_CONNECT_FAILED`
- `GIT_INSTALL_TIMED_OUT`
- `COMPUTE_CONFIG_INVALID`
- `COMPUTE_CONFIG_TARGET_REQUIRED`
- `COMPUTE_CONFIG_TARGET_UNKNOWN`
Expand Down Expand Up @@ -254,6 +258,10 @@ Recommended meanings:
- `LOCAL_STATE_WRITE_FAILED`: the CLI could not save local Project binding state such as `.prisma/local.json` or the matching `.gitignore` entry; callers should fix directory permissions or filesystem state before retrying
- `LOCAL_STATE_STALE`: local Project pin no longer matches platform data and continuing would be ambiguous
- `BRANCH_NOT_DEPLOYABLE`: command tried to deploy to a non-deployable branch context
- `GIT_ACCOUNT_REQUIRED`: `git account connect` needs a GitHub account in a non-interactive context; connectable accounts are in `error.meta.connectable` (and `error.meta.installUrl` when nothing is connectable) and as next steps
- `GIT_ACCOUNT_NOT_FOUND`: the requested GitHub account is not connectable to the active workspace
- `GIT_CONNECT_FAILED`: GitHub reports the installation no longer exists; stale platform records were cleaned up
- `GIT_INSTALL_TIMED_OUT`: the browser GitHub App install did not complete before the wait timed out; it may still finish, so re-check with `git account list`
- `COMPUTE_CONFIG_INVALID`: `prisma.compute.ts` failed to load or validate
- `COMPUTE_CONFIG_TARGET_REQUIRED`: a multi-app compute config needs an `[app]` target and none was given or inferred
- `COMPUTE_CONFIG_TARGET_UNKNOWN`: the `[app]` target matches no configured app
Expand Down
43 changes: 39 additions & 4 deletions packages/cli/fixtures/mock-api.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
{
"providers": [
{ "id": "github", "name": "GitHub" },
{ "id": "google", "name": "Google" }
{
"id": "github",
"name": "GitHub"
},
{
"id": "google",
"name": "Google"
}
],
"users": [
{
Expand Down Expand Up @@ -225,10 +231,39 @@
"end": "2026-06-30T23:59:59.999Z"
},
"metrics": {
"operations": { "used": 12500, "unit": "ops" },
"storage": { "used": 1.25, "unit": "GiB" }
"operations": {
"used": 12500,
"unit": "ops"
},
"storage": {
"used": 1.25,
"unit": "GiB"
}
},
"generatedAt": "2026-07-01T00:00:00.000Z"
}
],
"scmInstallations": [
{
"installationId": 555001,
"accountId": 700100,
"accountLogin": "acme-bot",
"accountType": "organization",
"workspaceId": "ws_123"
},
{
"installationId": 555002,
"accountId": 700200,
"accountLogin": "prisma-labs",
"accountType": "organization",
"workspaceId": "ws_456"
},
{
"installationId": 555003,
"accountId": 700200,
"accountLogin": "prisma-labs",
"accountType": "organization",
"workspaceId": "ws_456"
}
]
}
74 changes: 74 additions & 0 deletions packages/cli/src/adapters/mock-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ interface MembershipRecord {
workspaceId: string;
}

interface ScmInstallationRecord {
/** Numeric GitHub App installation id. */
installationId: number;
accountId: number;
accountLogin: string;
accountType: "user" | "organization";
suspended?: boolean;
workspaceId: string;
}

interface ProjectRecord {
id: string;
name: string;
Expand Down Expand Up @@ -93,6 +103,7 @@ interface MockApiData {
users: UserRecord[];
workspaces: WorkspaceRecord[];
memberships: MembershipRecord[];
scmInstallations?: ScmInstallationRecord[];
projects: ProjectRecord[];
branches: BranchRecord[];
deployments: DeploymentRecord[];
Expand Down Expand Up @@ -264,6 +275,69 @@ export class MockApi {
return { outcome: "transferred", project };
}

listScmInstallations(workspaceId: string) {
return (this.data.scmInstallations ?? [])
.filter((record) => record.workspaceId === workspaceId)
.map((record) => ({
installationId: record.installationId,
accountLogin: record.accountLogin,
accountType: record.accountType,
suspended: record.suspended ?? false,
}));
}

// Fixture sessions belong to every fixture workspace, so connectable means
// active rows of other workspaces, deduped per account with the last
// fixture entry (the newest) winning — mirroring the platform rule.
listConnectableScmInstallations(workspaceId: string) {
const rows = this.data.scmInstallations ?? [];
const accountsConnectedHere = new Set(
rows
.filter((record) => record.workspaceId === workspaceId)
.map((record) => record.accountId),
);
const seenAccounts = new Set<number>();
const connectable: { installationId: number; accountLogin: string }[] = [];
for (const record of [...rows].reverse()) {
if (record.workspaceId === workspaceId) continue;
if (accountsConnectedHere.has(record.accountId)) continue;
if (seenAccounts.has(record.accountId)) continue;
seenAccounts.add(record.accountId);
connectable.push({
installationId: record.installationId,
accountLogin: record.accountLogin,
});
}
return connectable;
}

connectScmInstallation(workspaceId: string, installationId: number) {
const source = (this.data.scmInstallations ?? []).find(
(record) => record.installationId === installationId,
);
if (!source) {
throw new Error(`Unknown fixture installation ${installationId}`);
}
const connected: ScmInstallationRecord = {
installationId: source.installationId,
accountId: source.accountId,
accountLogin: source.accountLogin,
accountType: source.accountType,
suspended: source.suspended ?? false,
workspaceId,
};
this.data.scmInstallations = [
...(this.data.scmInstallations ?? []),
connected,
];
return {
installationId: connected.installationId,
accountLogin: connected.accountLogin,
accountType: connected.accountType,
suspended: connected.suspended ?? false,
};
}

listBranchesForProject(projectId: string): BranchRecord[] {
return this.data.branches.filter(
(branch) => branch.projectId === projectId,
Expand Down
86 changes: 84 additions & 2 deletions packages/cli/src/commands/git/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { Command } from "commander";

import { runGitConnect, runGitDisconnect } from "../../controllers/project";
import {
runGitAccountConnect,
runGitAccountList,
runGitConnect,
runGitDisconnect,
} from "../../controllers/project";
import {
renderGitAccountConnect,
renderGitAccountList,
renderGitConnect,
renderGitDisconnect,
serializeGitAccountConnect,
serializeGitAccountList,
} from "../../presenters/project";
import { attachCommandDescriptor } from "../../shell/command-meta";
import { runCommand } from "../../shell/command-runner";
Expand All @@ -12,7 +21,11 @@ import {
addGlobalFlags,
} from "../../shell/global-flags";
import { type CliRuntime, configureRuntimeCommand } from "../../shell/runtime";
import type { ProjectRepositoryConnectionResult } from "../../types/project";
import type {
GitAccountsResult,
GitConnectAccountResult,
ProjectRepositoryConnectionResult,
} from "../../types/project";

export function createGitCommand(runtime: CliRuntime): Command {
const git = attachCommandDescriptor(
Expand All @@ -24,6 +37,7 @@ export function createGitCommand(runtime: CliRuntime): Command {

git.addCommand(createGitConnectCommand(runtime));
git.addCommand(createGitDisconnectCommand(runtime));
git.addCommand(createGitAccountCommand(runtime));

return git;
}
Expand Down Expand Up @@ -86,3 +100,71 @@ function createGitDisconnectCommand(runtime: CliRuntime): Command {

return command;
}

function createGitAccountCommand(runtime: CliRuntime): Command {
const account = attachCommandDescriptor(
configureRuntimeCommand(new Command("account"), runtime),
"git.account",
);

addCompactGlobalFlags(account);

account.addCommand(createGitAccountListCommand(runtime));
account.addCommand(createGitAccountConnectCommand(runtime));

return account;
}

function createGitAccountListCommand(runtime: CliRuntime): Command {
const command = attachCommandDescriptor(
configureRuntimeCommand(new Command("list"), runtime),
"git.account.list",
);

addGlobalFlags(command);

command.action(async (options) => {
await runCommand<GitAccountsResult>(
runtime,
"git.account.list",
options as Record<string, unknown>,
(context) => runGitAccountList(context),
{
renderHuman: (context, descriptor, result) =>
renderGitAccountList(context, descriptor, result),
renderJson: (result) => serializeGitAccountList(result),
},
);
});

return command;
}

function createGitAccountConnectCommand(runtime: CliRuntime): Command {
const command = attachCommandDescriptor(
configureRuntimeCommand(new Command("connect"), runtime),
"git.account.connect",
);

command.argument(
"[account]",
"GitHub account login or numeric installation id",
);
addGlobalFlags(command);

command.action(async (account: string | undefined, options) => {
await runCommand<GitConnectAccountResult>(
runtime,
"git.account.connect",
options as Record<string, unknown>,
(context) => runGitAccountConnect(context, account),
{
renderHuman: (context, descriptor, result) =>
renderGitAccountConnect(context, descriptor, result),
renderJson: (result) => serializeGitAccountConnect(result),
},
);
});

return command;
}
Loading
Loading