Adds `gh` CLI as a GitHub credential fallback for users who already have `gh auth login` configured locally. OpenChamber-owned OAuth credentials remain the primary source of truth; the `gh` token is only used when no stored OpenChamber GitHub access token exists and the fallback is not disabled. The fallback is implemented as a credential provider only: GitHub features continue to use the existing Octokit/GitHub API paths for issues, pull requests, checks, merges, and related operations. The PR does not replace those endpoints with `gh issue` or `gh pr` CLI commands. Server changes: - Add `gh-cli-credential.js` to read `gh auth token` with a bounded timeout. - Cache the `gh` token lookup for 30 seconds, including negative results, to avoid repeated subprocess spawning on status/polling paths. - Hide the subprocess window on Windows via `windowsHide: true`. - Clear the gh CLI token cache when the fallback setting changes. - Update `getOctokitOrNull()` to prefer stored OpenChamber OAuth tokens and fall back to the `gh` token only when enabled. - Add `ghCliDisabled` persistence in the existing settings file with atomic writes and `0o600` file permissions. - Add `POST /api/github/auth/gh-cli` to enable or disable the fallback. - Extend `/api/github/auth/status` with `ghCli` metadata: availability, disabled state, active state, and active user when applicable. UI/runtime changes: - Extend `GitHubAuthStatus` and `GitHubAPI` with gh CLI fallback metadata and toggle support. - Add web RuntimeAPI support for toggling the gh CLI fallback through `runtimeFetch`, preserving active runtime/remote target behavior. - Add deterministic VS Code unsupported handling for the gh CLI toggle. - Update GitHub Settings to show gh CLI availability and active status. - When gh CLI is the active auth source, show it in the connected account card and offer Disable instead of Disconnect. - Keep Add Account available so users can still connect an OpenChamber OAuth account, which then takes priority over gh CLI. - Add localized gh CLI settings strings across supported settings locales. Fixes addressed during review: - Removed unreachable UI branches in the inactive gh CLI card. - Avoided duplicate and repeated `gh auth token` subprocess calls. - Hardened settings file permissions for the new persisted flag. - Routed the gh CLI toggle through the RuntimeAPI/runtimeFetch path instead of direct browser `fetch`. - Added targeted tests for hidden subprocess options and negative-result cache behavior. - Fixed a VS Code webview Response body typing issue that blocked type-check.
72 lines
3.8 KiB
TypeScript
72 lines
3.8 KiB
TypeScript
import type {
|
|
GitHubAPI,
|
|
GitHubAuthStatus,
|
|
GitHubIssueCommentsResult,
|
|
GitHubIssueGetResult,
|
|
GitHubIssuesListResult,
|
|
GitHubPullRequestContextResult,
|
|
GitHubPullRequestsListResult,
|
|
GitHubPullRequest,
|
|
GitHubPullRequestCreateInput,
|
|
GitHubPullRequestMergeInput,
|
|
GitHubPullRequestMergeResult,
|
|
GitHubPullRequestReadyInput,
|
|
GitHubPullRequestReadyResult,
|
|
GitHubPullRequestUpdateInput,
|
|
GitHubPullRequestStatus,
|
|
GitHubDeviceFlowComplete,
|
|
GitHubDeviceFlowStart,
|
|
GitHubRepoUpstreamResult,
|
|
GitHubUserSummary,
|
|
} from '@openchamber/ui/lib/api/types';
|
|
|
|
import { sendBridgeMessage } from './bridge';
|
|
|
|
export const createVSCodeGitHubAPI = (): GitHubAPI => ({
|
|
authStatus: async () => sendBridgeMessage<GitHubAuthStatus>('api:github/auth:status'),
|
|
authStart: async () => sendBridgeMessage<GitHubDeviceFlowStart>('api:github/auth:start'),
|
|
authComplete: async (deviceCode: string) =>
|
|
sendBridgeMessage<GitHubDeviceFlowComplete>('api:github/auth:complete', { deviceCode }),
|
|
authDisconnect: async () => sendBridgeMessage<{ removed: boolean }>('api:github/auth:disconnect'),
|
|
authActivate: async (accountId: string) =>
|
|
sendBridgeMessage<GitHubAuthStatus>('api:github/auth:activate', { accountId }),
|
|
authSetGhCliDisabled: async () => {
|
|
throw new Error('gh CLI fallback is not supported in VS Code');
|
|
},
|
|
me: async () => sendBridgeMessage<GitHubUserSummary>('api:github/me'),
|
|
|
|
prStatus: async (directory: string, branch: string) =>
|
|
sendBridgeMessage<GitHubPullRequestStatus>('api:github/pr:status', { directory, branch }),
|
|
prCreate: async (payload: GitHubPullRequestCreateInput) =>
|
|
sendBridgeMessage<GitHubPullRequest>('api:github/pr:create', payload),
|
|
prUpdate: async (payload: GitHubPullRequestUpdateInput) =>
|
|
sendBridgeMessage<GitHubPullRequest>('api:github/pr:update', payload),
|
|
prMerge: async (payload: GitHubPullRequestMergeInput) =>
|
|
sendBridgeMessage<GitHubPullRequestMergeResult>('api:github/pr:merge', payload),
|
|
prReady: async (payload: GitHubPullRequestReadyInput) =>
|
|
sendBridgeMessage<GitHubPullRequestReadyResult>('api:github/pr:ready', payload),
|
|
|
|
issuesList: async (directory: string, options?: { page?: number; query?: string }) =>
|
|
sendBridgeMessage<GitHubIssuesListResult>('api:github/issues:list', { directory, page: options?.page ?? 1, query: options?.query ?? '' }),
|
|
issueGet: async (directory: string, number: number, options?: { sourceRepo?: { owner: string; repo: string } | null }) =>
|
|
sendBridgeMessage<GitHubIssueGetResult>('api:github/issues:get', { directory, number, sourceRepo: options?.sourceRepo ?? null }),
|
|
issueComments: async (directory: string, number: number, options?: { sourceRepo?: { owner: string; repo: string } | null }) =>
|
|
sendBridgeMessage<GitHubIssueCommentsResult>('api:github/issues:comments', { directory, number, sourceRepo: options?.sourceRepo ?? null }),
|
|
|
|
prsList: async (directory: string, options?: { page?: number; query?: string }) =>
|
|
sendBridgeMessage<GitHubPullRequestsListResult>('api:github/pulls:list', { directory, page: options?.page ?? 1, query: options?.query ?? '' }),
|
|
prContext: async (directory: string, number: number, options?: { includeDiff?: boolean; includeCheckDetails?: boolean; sourceRepo?: { owner: string; repo: string } | null }) =>
|
|
sendBridgeMessage<GitHubPullRequestContextResult>('api:github/pulls:context', {
|
|
directory,
|
|
number,
|
|
includeDiff: Boolean(options?.includeDiff),
|
|
includeCheckDetails: Boolean(options?.includeCheckDetails),
|
|
sourceRepo: options?.sourceRepo ?? null,
|
|
}),
|
|
|
|
repoUpstream: async (directory: string) =>
|
|
sendBridgeMessage<GitHubRepoUpstreamResult>('api:github/repo:upstream', { directory }),
|
|
repoBranches: async (owner: string, repo: string) =>
|
|
sendBridgeMessage<string[]>('api:github/repo:branches', { owner, repo }),
|
|
});
|