diff --git a/packages/ui/src/components/sections/openchamber/GitHubSettings.tsx b/packages/ui/src/components/sections/openchamber/GitHubSettings.tsx
index dd7a2398..c480a272 100644
--- a/packages/ui/src/components/sections/openchamber/GitHubSettings.tsx
+++ b/packages/ui/src/components/sections/openchamber/GitHubSettings.tsx
@@ -174,6 +174,30 @@ export const GitHubSettings: React.FC = () => {
};
}, [flow, pollIntervalMs, pollOnce, refreshStatus, runtimeGitHub, stopPolling, t]);
+ const toggleGhCli = React.useCallback(async (disabled: boolean) => {
+ setIsBusy(true);
+ try {
+ if (runtimeGitHub) {
+ await runtimeGitHub.authSetGhCliDisabled(disabled);
+ } else {
+ const response = await runtimeFetch('/api/github/auth/gh-cli', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
+ body: JSON.stringify({ disabled }),
+ });
+ const body = (await response.json().catch(() => null)) as { error?: string } | null;
+ if (!response.ok) throw new Error(body?.error || response.statusText);
+ }
+ toast.success(disabled ? t('settings.github.page.toast.ghCliDisabled') : t('settings.github.page.toast.ghCliEnabled'));
+ await refreshStatus(runtimeGitHub, { force: true });
+ } catch (error) {
+ console.error('Failed to update gh CLI setting:', error);
+ toast.error(t('settings.github.page.toast.ghCliUpdateFailed'));
+ } finally {
+ setIsBusy(false);
+ }
+ }, [refreshStatus, runtimeGitHub, t]);
+
const disconnect = React.useCallback(async () => {
setIsBusy(true);
try {
@@ -239,6 +263,7 @@ export const GitHubSettings: React.FC = () => {
const connected = Boolean(status?.connected);
const user = status?.user;
const accounts = status?.accounts ?? [];
+ const ghCli = status?.ghCli ?? null;
return (
@@ -287,12 +312,23 @@ export const GitHubSettings: React.FC = () => {
{t('settings.github.page.label.scopes', { value: status.scope })}
)}
+ {ghCli?.active && (
+
+ {t('settings.github.page.ghCli.activeDescription')}
+
+ )}
-
+ {ghCli?.active ? (
+
+ ) : (
+
+ )}
) : (
@@ -412,6 +448,41 @@ export const GitHubSettings: React.FC = () => {
)}
+
+ {ghCli?.available && !ghCli?.active && (
+
+
+ {t('settings.github.page.ghCli.title')}
+
+
+
+
+
+
+
+
+
+ {ghCli.disabled
+ ? t('settings.github.page.ghCli.disabledDescription')
+ : t('settings.github.page.ghCli.fallbackDescription')}
+
+
+
+
+
+
+
+ )}
);
};
diff --git a/packages/ui/src/lib/api/types.ts b/packages/ui/src/lib/api/types.ts
index cc0537c6..3e95dcc6 100644
--- a/packages/ui/src/lib/api/types.ts
+++ b/packages/ui/src/lib/api/types.ts
@@ -1039,6 +1039,12 @@ export type GitHubAuthStatus = {
user?: GitHubUserSummary | null;
scope?: string;
accounts?: GitHubAuthAccount[];
+ ghCli?: {
+ available: boolean;
+ disabled: boolean;
+ active: boolean;
+ user?: GitHubUserSummary | null;
+ } | null;
};
export type GitHubAuthAccount = {
@@ -1068,6 +1074,7 @@ export interface GitHubAPI {
authComplete(deviceCode: string): Promise;
authDisconnect(): Promise<{ removed: boolean }>;
authActivate(accountId: string): Promise;
+ authSetGhCliDisabled(disabled: boolean): Promise<{ disabled: boolean }>;
me?(): Promise;
prStatus(directory: string, branch: string, remote?: string, options?: { force?: boolean }): Promise;
diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts
index 2faf0add..a2c45ebc 100644
--- a/packages/ui/src/lib/i18n/messages/en.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/en.settings.ts
@@ -1407,6 +1407,15 @@ export const settingsDict = {
'settings.github.page.toast.disconnectFailed': 'Failed to disconnect GitHub',
'settings.github.page.toast.accountSwitched': 'GitHub account switched',
'settings.github.page.toast.accountSwitchFailed': 'Failed to switch GitHub account',
+ 'settings.github.page.ghCli.title': 'gh CLI',
+ 'settings.github.page.ghCli.activeDescription': 'Authenticated via the gh CLI',
+ 'settings.github.page.ghCli.fallbackDescription': 'Available as fallback if OpenChamber auth is removed',
+ 'settings.github.page.ghCli.disabledDescription': 'Installed but not used as a fallback',
+ 'settings.github.page.ghCli.actions.disable': 'Disable',
+ 'settings.github.page.ghCli.actions.enable': 'Enable',
+ 'settings.github.page.toast.ghCliEnabled': 'gh CLI fallback enabled',
+ 'settings.github.page.toast.ghCliDisabled': 'gh CLI fallback disabled',
+ 'settings.github.page.toast.ghCliUpdateFailed': 'Failed to update gh CLI setting',
'settings.notifications.page.delivery.title': 'Notification Delivery',
'settings.notifications.page.delivery.enableAria': 'Enable notifications',
'settings.notifications.page.delivery.enableLabel': 'Enable Notifications',
diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts
index 0f5088fd..127692bd 100644
--- a/packages/ui/src/lib/i18n/messages/es.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/es.settings.ts
@@ -1374,6 +1374,15 @@ export const settingsDict = {
"settings.github.page.toast.disconnectFailed": "No se pudo desconectar de GitHub",
"settings.github.page.toast.accountSwitched": "Cuenta de GitHub cambiada",
"settings.github.page.toast.accountSwitchFailed": "No se pudo cambiar de cuenta de GitHub",
+ "settings.github.page.ghCli.title": "gh CLI",
+ "settings.github.page.ghCli.activeDescription": "Autenticado a través de gh CLI",
+ "settings.github.page.ghCli.fallbackDescription": "Disponible como respaldo si se elimina la autenticación de OpenChamber",
+ "settings.github.page.ghCli.disabledDescription": "Instalado pero no usado como respaldo",
+ "settings.github.page.ghCli.actions.disable": "Desactivar",
+ "settings.github.page.ghCli.actions.enable": "Activar",
+ "settings.github.page.toast.ghCliEnabled": "Respaldo de gh CLI activado",
+ "settings.github.page.toast.ghCliDisabled": "Respaldo de gh CLI desactivado",
+ "settings.github.page.toast.ghCliUpdateFailed": "No se pudo actualizar la configuración de gh CLI",
"settings.notifications.page.delivery.title": "Entrega de notificaciones",
"settings.notifications.page.delivery.enableAria": "Habilitar notificaciones",
"settings.notifications.page.delivery.enableLabel": "Habilitar notificaciones",
diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts
index dd4ef3c7..86845342 100644
--- a/packages/ui/src/lib/i18n/messages/ko.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts
@@ -1374,6 +1374,15 @@ export const settingsDict = {
'settings.github.page.toast.disconnectFailed': 'GitHub 연결을 해제하지 못했습니다',
'settings.github.page.toast.accountSwitched': 'GitHub 계정이 전환되었습니다',
'settings.github.page.toast.accountSwitchFailed': 'GitHub 계정을 전환하지 못했습니다',
+ 'settings.github.page.ghCli.title': 'gh CLI',
+ 'settings.github.page.ghCli.activeDescription': 'gh CLI를 통해 인증됨',
+ 'settings.github.page.ghCli.fallbackDescription': 'OpenChamber 인증이 제거된 경우 대체로 사용 가능',
+ 'settings.github.page.ghCli.disabledDescription': '설치되어 있지만 대체로 사용되지 않음',
+ 'settings.github.page.ghCli.actions.disable': '비활성화',
+ 'settings.github.page.ghCli.actions.enable': '활성화',
+ 'settings.github.page.toast.ghCliEnabled': 'gh CLI 대체 활성화됨',
+ 'settings.github.page.toast.ghCliDisabled': 'gh CLI 대체 비활성화됨',
+ 'settings.github.page.toast.ghCliUpdateFailed': 'gh CLI 설정을 업데이트하지 못했습니다',
'settings.notifications.page.delivery.title': '알림',
'settings.notifications.page.delivery.enableAria': '알림 활성화',
'settings.notifications.page.delivery.enableLabel': '알림 활성화',
diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts
index c6587300..af1e2a9b 100644
--- a/packages/ui/src/lib/i18n/messages/pl.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts
@@ -246,6 +246,15 @@ export const settingsDict = {
'settings.github.page.toast.disconnectFailed': 'Nie udało się odłączyć GitHub',
'settings.github.page.toast.disconnected': 'Odłączono od GitHub',
'settings.github.page.toast.startConnectFailed': 'Nie udało się rozpocząć połączenia z GitHub',
+ 'settings.github.page.toast.ghCliEnabled': 'Rezerwa gh CLI włączona',
+ 'settings.github.page.toast.ghCliDisabled': 'Rezerwa gh CLI wyłączona',
+ 'settings.github.page.toast.ghCliUpdateFailed': 'Nie udało się zaktualizować ustawienia gh CLI',
+ 'settings.github.page.ghCli.title': 'gh CLI',
+ 'settings.github.page.ghCli.activeDescription': 'Uwierzytelniono przez gh CLI',
+ 'settings.github.page.ghCli.fallbackDescription': 'Dostępne jako rezerwa, jeśli uwierzytelnienie OpenChamber zostanie usunięte',
+ 'settings.github.page.ghCli.disabledDescription': 'Zainstalowane, ale nieużywane jako rezerwa',
+ 'settings.github.page.ghCli.actions.disable': 'Wyłącz',
+ 'settings.github.page.ghCli.actions.enable': 'Włącz',
'settings.github.page.tooltip.connectAccount': 'Połącz konto GitHub, aby korzystać z przepływów pracy dla PR i Issue w aplikacji.',
'settings.magicPrompts.page.actions.resetAllOverrides': 'Zresetuj wszystkie nadpisania',
'settings.magicPrompts.page.actions.resetToDefault': 'Zresetuj do domyślnych',
diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
index 4caa4cb1..a021ec35 100644
--- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
@@ -1374,6 +1374,15 @@ export const settingsDict = {
"settings.github.page.toast.disconnectFailed": "Não foi possível desconectar de GitHub",
"settings.github.page.toast.accountSwitched": "Conta de GitHub cambiada",
"settings.github.page.toast.accountSwitchFailed": "Não foi possível mudar de conta de GitHub",
+ "settings.github.page.ghCli.title": "gh CLI",
+ "settings.github.page.ghCli.activeDescription": "Autenticado via gh CLI",
+ "settings.github.page.ghCli.fallbackDescription": "Disponível como alternativa se a autenticação do OpenChamber for removida",
+ "settings.github.page.ghCli.disabledDescription": "Instalado, mas não usado como alternativa",
+ "settings.github.page.ghCli.actions.disable": "Desativar",
+ "settings.github.page.ghCli.actions.enable": "Ativar",
+ "settings.github.page.toast.ghCliEnabled": "Alternativa gh CLI ativada",
+ "settings.github.page.toast.ghCliDisabled": "Alternativa gh CLI desativada",
+ "settings.github.page.toast.ghCliUpdateFailed": "Falha ao atualizar configuração do gh CLI",
"settings.notifications.page.delivery.title": "Entrega de notificações",
"settings.notifications.page.delivery.enableAria": "Ativar notificações",
"settings.notifications.page.delivery.enableLabel": "Ativar notificações",
diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts
index eb413b0b..9252990a 100644
--- a/packages/ui/src/lib/i18n/messages/uk.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts
@@ -1374,6 +1374,15 @@ export const settingsDict = {
"settings.github.page.toast.disconnectFailed": "Не вдалося відключити GitHub",
"settings.github.page.toast.accountSwitched": "Обліковий запис GitHub змінено",
"settings.github.page.toast.accountSwitchFailed": "Не вдалося змінити обліковий запис GitHub",
+ "settings.github.page.ghCli.title": "gh CLI",
+ "settings.github.page.ghCli.activeDescription": "Автентифіковано через gh CLI",
+ "settings.github.page.ghCli.fallbackDescription": "Доступно як резервний варіант, якщо автентифікацію OpenChamber видалено",
+ "settings.github.page.ghCli.disabledDescription": "Встановлено, але не використовується як резервний варіант",
+ "settings.github.page.ghCli.actions.disable": "Вимкнути",
+ "settings.github.page.ghCli.actions.enable": "Увімкнути",
+ "settings.github.page.toast.ghCliEnabled": "Резервний варіант gh CLI увімкнено",
+ "settings.github.page.toast.ghCliDisabled": "Резервний варіант gh CLI вимкнено",
+ "settings.github.page.toast.ghCliUpdateFailed": "Не вдалося оновити налаштування gh CLI",
"settings.notifications.page.delivery.title": "Доставка сповіщень",
"settings.notifications.page.delivery.enableAria": "Увімкнути сповіщення",
"settings.notifications.page.delivery.enableLabel": "Увімкнути сповіщення",
diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
index d2e8b0ef..7210d246 100644
--- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
@@ -1374,6 +1374,15 @@ export const settingsDict = {
'settings.github.page.toast.disconnectFailed': '断开 GitHub 失败',
'settings.github.page.toast.accountSwitched': 'GitHub 账号已切换',
'settings.github.page.toast.accountSwitchFailed': '切换 GitHub 账号失败',
+ 'settings.github.page.ghCli.title': 'gh CLI',
+ 'settings.github.page.ghCli.activeDescription': '已通过 gh CLI 认证',
+ 'settings.github.page.ghCli.fallbackDescription': '在 OpenChamber 认证被移除时可作为备用',
+ 'settings.github.page.ghCli.disabledDescription': '已安装但未作为备用使用',
+ 'settings.github.page.ghCli.actions.disable': '禁用',
+ 'settings.github.page.ghCli.actions.enable': '启用',
+ 'settings.github.page.toast.ghCliEnabled': 'gh CLI 备用已启用',
+ 'settings.github.page.toast.ghCliDisabled': 'gh CLI 备用已禁用',
+ 'settings.github.page.toast.ghCliUpdateFailed': '更新 gh CLI 设置失败',
'settings.notifications.page.delivery.title': '通知投递',
'settings.notifications.page.delivery.enableAria': '启用通知',
'settings.notifications.page.delivery.enableLabel': '启用通知',
diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts
index 2090b254..116bae0d 100644
--- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts
@@ -1295,6 +1295,15 @@
'settings.github.page.toast.disconnectFailed': '中斷 GitHub 失敗',
'settings.github.page.toast.accountSwitched': 'GitHub 帳號已切換',
'settings.github.page.toast.accountSwitchFailed': '切換 GitHub 帳號失敗',
+ 'settings.github.page.ghCli.title': 'gh CLI',
+ 'settings.github.page.ghCli.activeDescription': '已透過 gh CLI 認證',
+ 'settings.github.page.ghCli.fallbackDescription': '在 OpenChamber 認證被移除時可作為備用',
+ 'settings.github.page.ghCli.disabledDescription': '已安裝但未作為備用使用',
+ 'settings.github.page.ghCli.actions.disable': '停用',
+ 'settings.github.page.ghCli.actions.enable': '啟用',
+ 'settings.github.page.toast.ghCliEnabled': 'gh CLI 備用已啟用',
+ 'settings.github.page.toast.ghCliDisabled': 'gh CLI 備用已停用',
+ 'settings.github.page.toast.ghCliUpdateFailed': '更新 gh CLI 設定失敗',
'settings.notifications.page.delivery.title': '通知傳遞',
'settings.notifications.page.delivery.enableAria': '啟用通知',
'settings.notifications.page.delivery.enableLabel': '啟用通知',
diff --git a/packages/vscode/webview/api/github.ts b/packages/vscode/webview/api/github.ts
index 743cff04..34666bf1 100644
--- a/packages/vscode/webview/api/github.ts
+++ b/packages/vscode/webview/api/github.ts
@@ -30,6 +30,9 @@ export const createVSCodeGitHubAPI = (): GitHubAPI => ({
authDisconnect: async () => sendBridgeMessage<{ removed: boolean }>('api:github/auth:disconnect'),
authActivate: async (accountId: string) =>
sendBridgeMessage('api:github/auth:activate', { accountId }),
+ authSetGhCliDisabled: async () => {
+ throw new Error('gh CLI fallback is not supported in VS Code');
+ },
me: async () => sendBridgeMessage('api:github/me'),
prStatus: async (directory: string, branch: string) =>
diff --git a/packages/web/server/lib/github/auth.js b/packages/web/server/lib/github/auth.js
index 6ac17865..9e0eeb83 100644
--- a/packages/web/server/lib/github/auth.js
+++ b/packages/web/server/lib/github/auth.js
@@ -305,3 +305,41 @@ export function getGitHubScopes() {
}
export const GITHUB_AUTH_FILE = STORAGE_FILE;
+
+export function isGhCliDisabled() {
+ try {
+ if (fs.existsSync(SETTINGS_FILE)) {
+ const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
+ return Boolean(parsed?.ghCliDisabled);
+ }
+ } catch {
+ // ignore
+ }
+ return false;
+}
+
+export function setGhCliDisabled(disabled) {
+ ensureStorageDir();
+ let settings = {};
+ try {
+ if (fs.existsSync(SETTINGS_FILE)) {
+ settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')) || {};
+ }
+ } catch {
+ // ignore
+ }
+ settings.ghCliDisabled = Boolean(disabled);
+ const tmpFile = `${SETTINGS_FILE}.${process.pid}.${Date.now()}.tmp`;
+ fs.writeFileSync(tmpFile, JSON.stringify(settings, null, 2), 'utf8');
+ try {
+ fs.chmodSync(tmpFile, 0o600);
+ } catch {
+ // best-effort
+ }
+ fs.renameSync(tmpFile, SETTINGS_FILE);
+ try {
+ fs.chmodSync(SETTINGS_FILE, 0o600);
+ } catch {
+ // best-effort
+ }
+}
diff --git a/packages/web/server/lib/github/gh-cli-credential.js b/packages/web/server/lib/github/gh-cli-credential.js
new file mode 100644
index 00000000..cf77b4d2
--- /dev/null
+++ b/packages/web/server/lib/github/gh-cli-credential.js
@@ -0,0 +1,38 @@
+import { execFileSync } from 'child_process';
+
+const CACHE_TTL_MS = 30_000;
+let cachedToken = null;
+let cachedAt = 0;
+let hasCachedToken = false;
+
+function fetchGhCliToken() {
+ try {
+ const token = execFileSync('gh', ['auth', 'token'], {
+ encoding: 'utf8',
+ stdio: ['pipe', 'pipe', 'pipe'],
+ timeout: 5000,
+ windowsHide: true,
+ }).trim();
+ return token || null;
+ } catch {
+ return null;
+ }
+}
+
+export function getGhCliToken() {
+ const now = Date.now();
+ if (hasCachedToken && now - cachedAt < CACHE_TTL_MS) {
+ return cachedToken;
+ }
+ const token = fetchGhCliToken();
+ cachedToken = token;
+ cachedAt = now;
+ hasCachedToken = true;
+ return token;
+}
+
+export function clearGhCliTokenCache() {
+ cachedToken = null;
+ cachedAt = 0;
+ hasCachedToken = false;
+}
diff --git a/packages/web/server/lib/github/gh-cli-credential.test.js b/packages/web/server/lib/github/gh-cli-credential.test.js
new file mode 100644
index 00000000..2ee18222
--- /dev/null
+++ b/packages/web/server/lib/github/gh-cli-credential.test.js
@@ -0,0 +1,43 @@
+import { beforeEach, describe, expect, mock, test } from 'bun:test';
+
+const execFileSyncMock = mock(() => '');
+
+mock.module('child_process', () => ({
+ execFileSync: execFileSyncMock,
+}));
+
+const { clearGhCliTokenCache, getGhCliToken } = await import('./gh-cli-credential.js');
+
+describe('gh CLI credential lookup', () => {
+ beforeEach(() => {
+ execFileSyncMock.mockReset();
+ clearGhCliTokenCache();
+ });
+
+ test('hides the subprocess window on Windows', () => {
+ execFileSyncMock.mockReturnValueOnce('token\n');
+
+ expect(getGhCliToken()).toBe('token');
+ expect(execFileSyncMock).toHaveBeenCalledWith('gh', ['auth', 'token'], {
+ encoding: 'utf8',
+ stdio: ['pipe', 'pipe', 'pipe'],
+ timeout: 5000,
+ windowsHide: true,
+ });
+ });
+
+ test('caches unavailable gh CLI result until cache is cleared', () => {
+ execFileSyncMock.mockImplementation(() => {
+ throw new Error('gh unavailable');
+ });
+
+ expect(getGhCliToken()).toBeNull();
+ expect(getGhCliToken()).toBeNull();
+ expect(execFileSyncMock).toHaveBeenCalledTimes(1);
+
+ clearGhCliTokenCache();
+
+ expect(getGhCliToken()).toBeNull();
+ expect(execFileSyncMock).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/packages/web/server/lib/github/index.js b/packages/web/server/lib/github/index.js
index b86c4749..075748b6 100644
--- a/packages/web/server/lib/github/index.js
+++ b/packages/web/server/lib/github/index.js
@@ -6,6 +6,8 @@ export {
clearGitHubAuth,
getGitHubClientId,
getGitHubScopes,
+ isGhCliDisabled,
+ setGhCliDisabled,
GITHUB_AUTH_FILE,
} from './auth.js';
diff --git a/packages/web/server/lib/github/octokit.js b/packages/web/server/lib/github/octokit.js
index e374cfa5..5ee1e2e7 100644
--- a/packages/web/server/lib/github/octokit.js
+++ b/packages/web/server/lib/github/octokit.js
@@ -1,10 +1,12 @@
import { Octokit } from '@octokit/rest';
-import { getGitHubAuth } from './auth.js';
+import { getGitHubAuth, isGhCliDisabled } from './auth.js';
+import { getGhCliToken } from './gh-cli-credential.js';
export function getOctokitOrNull() {
const auth = getGitHubAuth();
- if (!auth?.accessToken) {
+ const token = auth?.accessToken || (!isGhCliDisabled() ? getGhCliToken() : null);
+ if (!token) {
return null;
}
- return new Octokit({ auth: auth.accessToken });
+ return new Octokit({ auth: token });
}
diff --git a/packages/web/server/lib/github/routes.js b/packages/web/server/lib/github/routes.js
index 5cf10c56..a23f8ab4 100644
--- a/packages/web/server/lib/github/routes.js
+++ b/packages/web/server/lib/github/routes.js
@@ -76,16 +76,25 @@ export function registerGitHubRoutes(app) {
app.get('/api/github/auth/status', async (_req, res) => {
try {
- const { getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts } = await getGitHubLibraries();
+ const { getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts, isGhCliDisabled } = await getGitHubLibraries();
+ const { getGhCliToken } = await import('./gh-cli-credential.js');
+
const auth = getGitHubAuth();
const accounts = getGitHubAuthAccounts();
- if (!auth?.accessToken) {
- return res.json({ connected: false, accounts });
- }
+ const ghCliDisabled = isGhCliDisabled();
+ const ghToken = getGhCliToken();
+ const usingOwnToken = Boolean(auth?.accessToken);
+
+ const buildGhCli = (activeUser = null) => ({
+ available: ghToken !== null,
+ disabled: ghCliDisabled,
+ active: !usingOwnToken && ghToken !== null && !ghCliDisabled,
+ ...(activeUser ? { user: activeUser } : {}),
+ });
const octokit = getOctokitOrNull();
if (!octokit) {
- return res.json({ connected: false, accounts });
+ return res.json({ connected: false, accounts, ghCli: buildGhCli() });
}
let user = null;
@@ -93,19 +102,21 @@ export function registerGitHubRoutes(app) {
user = await getGitHubUserSummary(octokit);
} catch (error) {
if (isGitHubAuthInvalid(error)) {
- clearGitHubAuth();
- return res.json({ connected: false, accounts: getGitHubAuthAccounts() });
+ if (usingOwnToken) clearGitHubAuth();
+ return res.json({ connected: false, accounts: getGitHubAuthAccounts(), ghCli: buildGhCli() });
}
}
- const fallback = auth.user;
+ const fallback = usingOwnToken ? auth.user : null;
const mergedUser = user || fallback;
+ const ghIsActive = !usingOwnToken && ghToken !== null && !ghCliDisabled;
return res.json({
connected: true,
user: mergedUser,
- scope: auth.scope,
+ scope: usingOwnToken ? auth.scope : undefined,
accounts,
+ ghCli: buildGhCli(ghIsActive ? mergedUser : null),
});
} catch (error) {
console.error('Failed to get GitHub auth status:', error);
@@ -113,6 +124,20 @@ export function registerGitHubRoutes(app) {
}
});
+ app.post('/api/github/auth/gh-cli', async (req, res) => {
+ try {
+ const { setGhCliDisabled, isGhCliDisabled } = await getGitHubLibraries();
+ const { clearGhCliTokenCache } = await import('./gh-cli-credential.js');
+ const disabled = Boolean(req.body?.disabled);
+ setGhCliDisabled(disabled);
+ clearGhCliTokenCache();
+ return res.json({ disabled: isGhCliDisabled() });
+ } catch (error) {
+ console.error('Failed to update gh CLI setting:', error);
+ return res.status(500).json({ error: error.message || 'Failed to update gh CLI setting' });
+ }
+ });
+
app.post('/api/github/auth/start', async (_req, res) => {
try {
const { getGitHubClientId, getGitHubScopes, startDeviceFlow } = await getGitHubLibraries();
diff --git a/packages/web/src/api/github.ts b/packages/web/src/api/github.ts
index 80b7c7dc..6587a2bf 100644
--- a/packages/web/src/api/github.ts
+++ b/packages/web/src/api/github.ts
@@ -88,6 +88,19 @@ export const createWebGitHubAPI = ({ urls }: WebGitHubAPIOptions): GitHubAPI =>
return payload;
},
+ async authSetGhCliDisabled(disabled: boolean): Promise<{ disabled: boolean }> {
+ const response = await runtimeFetch('/api/github/auth/gh-cli', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
+ body: JSON.stringify({ disabled }),
+ });
+ const payload = await jsonOrNull<{ disabled?: boolean; error?: string }>(response);
+ if (!response.ok || !payload) {
+ throw new Error(payload?.error || response.statusText || 'Failed to update gh CLI setting');
+ }
+ return { disabled: Boolean(payload.disabled) };
+ },
+
async me(): Promise {
const response = await runtimeFetch('/api/github/me', { method: 'GET', headers: { Accept: 'application/json' } });
const payload = await jsonOrNull(response);